• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.companion.association;
18 
19 import static com.android.server.companion.utils.MetricUtils.logCreateAssociation;
20 import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation;
21 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
22 
23 import android.annotation.IntDef;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.SuppressLint;
27 import android.annotation.UserIdInt;
28 import android.companion.AssociationInfo;
29 import android.companion.IOnAssociationsChangedListener;
30 import android.content.Context;
31 import android.content.pm.UserInfo;
32 import android.net.MacAddress;
33 import android.os.Binder;
34 import android.os.RemoteCallbackList;
35 import android.os.RemoteException;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.util.Slog;
39 
40 import com.android.internal.annotations.GuardedBy;
41 import com.android.internal.util.CollectionUtils;
42 
43 import java.io.PrintWriter;
44 import java.lang.annotation.Retention;
45 import java.lang.annotation.RetentionPolicy;
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.LinkedHashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.Set;
53 import java.util.concurrent.ExecutorService;
54 import java.util.concurrent.Executors;
55 
56 /**
57  * Association store for CRUD.
58  */
59 @SuppressLint("LongLogTag")
60 public class AssociationStore {
61 
62     @IntDef(prefix = {"CHANGE_TYPE_"}, value = {
63             CHANGE_TYPE_ADDED,
64             CHANGE_TYPE_REMOVED,
65             CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
66             CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
67     })
68     @Retention(RetentionPolicy.SOURCE)
69     public @interface ChangeType {
70     }
71 
72     public static final int CHANGE_TYPE_ADDED = 0;
73     public static final int CHANGE_TYPE_REMOVED = 1;
74     public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
75     public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;
76 
77     /** Listener for any changes to associations. */
78     public interface OnChangeListener {
79         /**
80          * Called when there are association changes.
81          */
onAssociationChanged( @ssociationStore.ChangeType int changeType, AssociationInfo association)82         default void onAssociationChanged(
83                 @AssociationStore.ChangeType int changeType, AssociationInfo association) {
84             switch (changeType) {
85                 case CHANGE_TYPE_ADDED:
86                     onAssociationAdded(association);
87                     break;
88 
89                 case CHANGE_TYPE_REMOVED:
90                     onAssociationRemoved(association);
91                     break;
92 
93                 case CHANGE_TYPE_UPDATED_ADDRESS_CHANGED:
94                     onAssociationUpdated(association, true);
95                     break;
96 
97                 case CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED:
98                     onAssociationUpdated(association, false);
99                     break;
100             }
101         }
102 
103         /**
104          * Called when an association is added.
105          */
onAssociationAdded(AssociationInfo association)106         default void onAssociationAdded(AssociationInfo association) {
107         }
108 
109         /**
110          * Called when an association is removed.
111          */
onAssociationRemoved(AssociationInfo association)112         default void onAssociationRemoved(AssociationInfo association) {
113         }
114 
115         /**
116          * Called when an association is updated.
117          */
onAssociationUpdated(AssociationInfo association, boolean addressChanged)118         default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {
119         }
120     }
121 
122     private static final String TAG = "CDM_AssociationStore";
123 
124     private final Context mContext;
125     private final UserManager mUserManager;
126     private final AssociationDiskStore mDiskStore;
127     private final ExecutorService mExecutor;
128 
129     private final Object mLock = new Object();
130     @GuardedBy("mLock")
131     private boolean mPersisted = false;
132     @GuardedBy("mLock")
133     private final Map<Integer, AssociationInfo> mIdToAssociationMap = new HashMap<>();
134     @GuardedBy("mLock")
135     private int mMaxId = 0;
136 
137     @GuardedBy("mLocalListeners")
138     private final Set<OnChangeListener> mLocalListeners = new LinkedHashSet<>();
139     @GuardedBy("mRemoteListeners")
140     private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners =
141             new RemoteCallbackList<>();
142 
AssociationStore(Context context, UserManager userManager, AssociationDiskStore diskStore)143     public AssociationStore(Context context, UserManager userManager,
144             AssociationDiskStore diskStore) {
145         mContext = context;
146         mUserManager = userManager;
147         mDiskStore = diskStore;
148         mExecutor = Executors.newSingleThreadExecutor();
149     }
150 
151     /**
152      * Load all alive users' associations from disk to cache.
153      */
refreshCache()154     public void refreshCache() {
155         Binder.withCleanCallingIdentity(() -> {
156             List<Integer> userIds = new ArrayList<>();
157             for (UserInfo user : mUserManager.getAliveUsers()) {
158                 userIds.add(user.id);
159             }
160 
161             synchronized (mLock) {
162                 mPersisted = false;
163 
164                 mIdToAssociationMap.clear();
165                 mMaxId = 0;
166 
167                 // The data is stored in DE directories, so we can read the data for all users now
168                 // (which would not be possible if the data was stored to CE directories).
169                 Map<Integer, Associations> userToAssociationsMap =
170                         mDiskStore.readAssociationsByUsers(userIds);
171                 for (Map.Entry<Integer, Associations> entry : userToAssociationsMap.entrySet()) {
172                     for (AssociationInfo association : entry.getValue().getAssociations()) {
173                         mIdToAssociationMap.put(association.getId(), association);
174                     }
175                     mMaxId = Math.max(mMaxId, entry.getValue().getMaxId());
176                 }
177 
178                 mPersisted = true;
179             }
180         });
181     }
182 
183     /**
184      * Get the current max association id.
185      */
getMaxId()186     public int getMaxId() {
187         synchronized (mLock) {
188             return mMaxId;
189         }
190     }
191 
192     /**
193      * Get the next available association id.
194      */
getNextId()195     public int getNextId() {
196         synchronized (mLock) {
197             return getMaxId() + 1;
198         }
199     }
200 
201     /**
202      * Add an association.
203      */
addAssociation(@onNull AssociationInfo association)204     public void addAssociation(@NonNull AssociationInfo association) {
205         Slog.i(TAG, "Adding new association=[" + association + "]...");
206 
207         final int id = association.getId();
208         final int userId = association.getUserId();
209 
210         synchronized (mLock) {
211             if (mIdToAssociationMap.containsKey(id)) {
212                 Slog.e(TAG, "Association id=[" + id + "] already exists.");
213                 return;
214             }
215 
216             mIdToAssociationMap.put(id, association);
217             mMaxId = Math.max(mMaxId, id);
218 
219             writeCacheToDisk(userId);
220 
221             Slog.i(TAG, "Done adding new association.");
222         }
223 
224         logCreateAssociation(association.getDeviceProfile());
225 
226         if (association.isActive()) {
227             broadcastChange(CHANGE_TYPE_ADDED, association);
228         }
229     }
230 
231     /**
232      * Update an association.
233      */
updateAssociation(@onNull AssociationInfo updated)234     public void updateAssociation(@NonNull AssociationInfo updated) {
235         Slog.i(TAG, "Updating new association=[" + updated + "]...");
236 
237         final int id = updated.getId();
238         final AssociationInfo current;
239         final boolean macAddressChanged;
240 
241         synchronized (mLock) {
242             current = mIdToAssociationMap.get(id);
243             if (current == null) {
244                 Slog.w(TAG, "Can't update association id=[" + id + "]. It does not exist.");
245                 return;
246             }
247 
248             if (current.equals(updated)) {
249                 Slog.w(TAG, "Association is the same.");
250                 return;
251             }
252 
253             mIdToAssociationMap.put(id, updated);
254 
255             writeCacheToDisk(updated.getUserId());
256         }
257 
258         Slog.i(TAG, "Done updating association.");
259 
260         if (current.isActive() && !updated.isActive()) {
261             broadcastChange(CHANGE_TYPE_REMOVED, updated);
262             return;
263         }
264 
265         if (updated.isActive()) {
266             // Check if the MacAddress has changed.
267             final MacAddress updatedAddress = updated.getDeviceMacAddress();
268             final MacAddress currentAddress = current.getDeviceMacAddress();
269             macAddressChanged = !Objects.equals(currentAddress, updatedAddress);
270 
271             broadcastChange(macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED
272                     : CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, updated);
273         }
274     }
275 
276     /**
277      * Remove an association.
278      */
removeAssociation(int id, String reason)279     public void removeAssociation(int id, String reason) {
280         Slog.i(TAG, "Removing association id=[" + id + "]...");
281 
282         final AssociationInfo association;
283 
284         synchronized (mLock) {
285             association = mIdToAssociationMap.remove(id);
286 
287             if (association == null) {
288                 Slog.w(TAG, "Can't remove association id=[" + id + "]. It does not exist.");
289                 return;
290             }
291 
292             writeCacheToDisk(association.getUserId());
293 
294             mDiskStore.writeLastRemovedAssociation(association, reason);
295 
296             Slog.i(TAG, "Done removing association.");
297         }
298 
299         logRemoveAssociation(association.getDeviceProfile());
300 
301         if (association.isActive()) {
302             broadcastChange(CHANGE_TYPE_REMOVED, association);
303         }
304     }
305 
writeCacheToDisk(@serIdInt int userId)306     private void writeCacheToDisk(@UserIdInt int userId) {
307         mExecutor.execute(() -> {
308             Associations associations = new Associations();
309             synchronized (mLock) {
310                 associations.setMaxId(mMaxId);
311                 associations.setAssociations(
312                         CollectionUtils.filter(mIdToAssociationMap.values().stream().toList(),
313                                 a -> a.getUserId() == userId));
314             }
315             mDiskStore.writeAssociationsForUser(userId, associations);
316         });
317     }
318 
319     /**
320      * Get a copy of all associations including pending and revoked ones.
321      * Modifying the copy won't modify the actual associations.
322      *
323      * If a cache miss happens, read from disk.
324      */
325     @NonNull
getAssociations()326     public List<AssociationInfo> getAssociations() {
327         synchronized (mLock) {
328             if (!mPersisted) {
329                 refreshCache();
330             }
331             return List.copyOf(mIdToAssociationMap.values());
332         }
333     }
334 
335     /**
336      * Get a copy of active associations.
337      */
338     @NonNull
getActiveAssociations()339     public List<AssociationInfo> getActiveAssociations() {
340         synchronized (mLock) {
341             return CollectionUtils.filter(getAssociations(), AssociationInfo::isActive);
342         }
343     }
344 
345     /**
346      * Get a copy of all associations by user.
347      */
348     @NonNull
getAssociationsByUser(@serIdInt int userId)349     public List<AssociationInfo> getAssociationsByUser(@UserIdInt int userId) {
350         synchronized (mLock) {
351             return CollectionUtils.filter(getAssociations(), a -> a.getUserId() == userId);
352         }
353     }
354 
355     /**
356      * Get a copy of active associations by user.
357      */
358     @NonNull
getActiveAssociationsByUser(@serIdInt int userId)359     public List<AssociationInfo> getActiveAssociationsByUser(@UserIdInt int userId) {
360         synchronized (mLock) {
361             return CollectionUtils.filter(getActiveAssociations(), a -> a.getUserId() == userId);
362         }
363     }
364 
365     /**
366      * Get a copy of all associations by package.
367      */
368     @NonNull
getAssociationsByPackage(@serIdInt int userId, @NonNull String packageName)369     public List<AssociationInfo> getAssociationsByPackage(@UserIdInt int userId,
370             @NonNull String packageName) {
371         synchronized (mLock) {
372             return CollectionUtils.filter(getAssociationsByUser(userId),
373                     a -> a.getPackageName().equals(packageName));
374         }
375     }
376 
377     /**
378      * Get a copy of active associations by package.
379      */
380     @NonNull
getActiveAssociationsByPackage(@serIdInt int userId, @NonNull String packageName)381     public List<AssociationInfo> getActiveAssociationsByPackage(@UserIdInt int userId,
382             @NonNull String packageName) {
383         synchronized (mLock) {
384             return CollectionUtils.filter(getActiveAssociationsByUser(userId),
385                     a -> a.getPackageName().equals(packageName));
386         }
387     }
388 
389     /**
390      * Get the first active association with the mac address.
391      */
392     @Nullable
getFirstAssociationByAddress( @serIdInt int userId, @NonNull String packageName, @NonNull String macAddress)393     public AssociationInfo getFirstAssociationByAddress(
394             @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
395         synchronized (mLock) {
396             return CollectionUtils.find(getActiveAssociationsByPackage(userId, packageName),
397                     a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress()
398                             .equals(MacAddress.fromString(macAddress)));
399         }
400     }
401 
402     /**
403      * Get the association by id.
404      */
405     @Nullable
getAssociationById(int id)406     public AssociationInfo getAssociationById(int id) {
407         synchronized (mLock) {
408             return mIdToAssociationMap.get(id);
409         }
410     }
411 
412     /**
413      * Get a copy of active associations by mac address.
414      */
415     @NonNull
getActiveAssociationsByAddress(@onNull String macAddress)416     public List<AssociationInfo> getActiveAssociationsByAddress(@NonNull String macAddress) {
417         synchronized (mLock) {
418             return CollectionUtils.filter(getActiveAssociations(),
419                     a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress()
420                             .equals(MacAddress.fromString(macAddress)));
421         }
422     }
423 
424     /**
425      * Get a copy of revoked associations.
426      */
427     @NonNull
getRevokedAssociations()428     public List<AssociationInfo> getRevokedAssociations() {
429         synchronized (mLock) {
430             return CollectionUtils.filter(getAssociations(), AssociationInfo::isRevoked);
431         }
432     }
433 
434     /**
435      * Get a copy of revoked associations for the package.
436      */
437     @NonNull
getRevokedAssociations(@serIdInt int userId, @NonNull String packageName)438     public List<AssociationInfo> getRevokedAssociations(@UserIdInt int userId,
439             @NonNull String packageName) {
440         synchronized (mLock) {
441             return CollectionUtils.filter(getAssociations(),
442                     a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId
443                             && a.isRevoked());
444         }
445     }
446 
447     /**
448      * Get a copy of active associations.
449      */
450     @NonNull
getPendingAssociations(@serIdInt int userId, @NonNull String packageName)451     public List<AssociationInfo> getPendingAssociations(@UserIdInt int userId,
452             @NonNull String packageName) {
453         synchronized (mLock) {
454             return CollectionUtils.filter(getAssociations(),
455                     a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId
456                             && a.isPending());
457         }
458     }
459 
460     /**
461      * Get association by id with caller checks.
462      *
463      * If the association is not found, an IllegalArgumentException would be thrown.
464      *
465      * If the caller can't access the association, a SecurityException would be thrown.
466      */
467     @NonNull
getAssociationWithCallerChecks(int associationId)468     public AssociationInfo getAssociationWithCallerChecks(int associationId) {
469         AssociationInfo association = getAssociationById(associationId);
470         if (association == null) {
471             throw new IllegalArgumentException(
472                     "getAssociationWithCallerChecks() Association id=[" + associationId
473                             + "] doesn't exist.");
474         }
475         enforceCallerCanManageAssociationsForPackage(mContext, association.getUserId(),
476                 association.getPackageName(), null);
477         return association;
478     }
479 
480     /**
481      * Register a local listener for association changes.
482      */
registerLocalListener(@onNull OnChangeListener listener)483     public void registerLocalListener(@NonNull OnChangeListener listener) {
484         synchronized (mLocalListeners) {
485             mLocalListeners.add(listener);
486         }
487     }
488 
489     /**
490      * Unregister a local listener previously registered for association changes.
491      */
unregisterLocalListener(@onNull OnChangeListener listener)492     public void unregisterLocalListener(@NonNull OnChangeListener listener) {
493         synchronized (mLocalListeners) {
494             mLocalListeners.remove(listener);
495         }
496     }
497 
498     /**
499      * Register a remote listener for association changes.
500      */
registerRemoteListener(@onNull IOnAssociationsChangedListener listener, int userId)501     public void registerRemoteListener(@NonNull IOnAssociationsChangedListener listener,
502             int userId) {
503         synchronized (mRemoteListeners) {
504             mRemoteListeners.register(listener, userId);
505         }
506     }
507 
508     /**
509      * Unregister a remote listener previously registered for association changes.
510      */
unregisterRemoteListener(@onNull IOnAssociationsChangedListener listener)511     public void unregisterRemoteListener(@NonNull IOnAssociationsChangedListener listener) {
512         synchronized (mRemoteListeners) {
513             mRemoteListeners.unregister(listener);
514         }
515     }
516 
517     /**
518      * Dumps current companion device association states.
519      */
dump(@onNull PrintWriter out)520     public void dump(@NonNull PrintWriter out) {
521         out.append("Companion Device Associations: ");
522         if (getActiveAssociations().isEmpty()) {
523             out.append("<empty>\n");
524         } else {
525             out.append("\n");
526             for (AssociationInfo a : getActiveAssociations()) {
527                 out.append("  ").append(a.toString()).append('\n');
528             }
529         }
530 
531         out.append("Last Removed Association:\n");
532         for (UserInfo user : mUserManager.getAliveUsers()) {
533             String lastRemovedAssociation = mDiskStore.readLastRemovedAssociation(user.id);
534             if (lastRemovedAssociation != null) {
535                 out.append("  ").append(lastRemovedAssociation).append('\n');
536             }
537         }
538     }
539 
broadcastChange(@hangeType int changeType, AssociationInfo association)540     private void broadcastChange(@ChangeType int changeType, AssociationInfo association) {
541         Slog.i(TAG, "Broadcasting association changes - changeType=[" + changeType + "]...");
542 
543         synchronized (mLocalListeners) {
544             for (OnChangeListener listener : mLocalListeners) {
545                 listener.onAssociationChanged(changeType, association);
546             }
547         }
548         synchronized (mRemoteListeners) {
549             final int userId = association.getUserId();
550             final List<AssociationInfo> updatedAssociations = getActiveAssociationsByUser(userId);
551             // Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED.
552             // Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in
553             // association's configs, which "listeners" won't (and shouldn't) be able to see.
554             if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) {
555                 mRemoteListeners.broadcast((listener, callbackUserId) -> {
556                     int listenerUserId = (int) callbackUserId;
557                     if (listenerUserId == userId || listenerUserId == UserHandle.USER_ALL) {
558                         try {
559                             listener.onAssociationsChanged(updatedAssociations);
560                         } catch (RemoteException ignored) {
561                         }
562                     }
563                 });
564             }
565         }
566     }
567 }
568