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)279     public void removeAssociation(int id) {
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             Slog.i(TAG, "Done removing association.");
295         }
296 
297         logRemoveAssociation(association.getDeviceProfile());
298 
299         if (association.isActive()) {
300             broadcastChange(CHANGE_TYPE_REMOVED, association);
301         }
302     }
303 
writeCacheToDisk(@serIdInt int userId)304     private void writeCacheToDisk(@UserIdInt int userId) {
305         mExecutor.execute(() -> {
306             Associations associations = new Associations();
307             synchronized (mLock) {
308                 associations.setMaxId(mMaxId);
309                 associations.setAssociations(
310                         CollectionUtils.filter(mIdToAssociationMap.values().stream().toList(),
311                                 a -> a.getUserId() == userId));
312             }
313             mDiskStore.writeAssociationsForUser(userId, associations);
314         });
315     }
316 
317     /**
318      * Get a copy of all associations including pending and revoked ones.
319      * Modifying the copy won't modify the actual associations.
320      *
321      * If a cache miss happens, read from disk.
322      */
323     @NonNull
getAssociations()324     public List<AssociationInfo> getAssociations() {
325         synchronized (mLock) {
326             if (!mPersisted) {
327                 refreshCache();
328             }
329             return List.copyOf(mIdToAssociationMap.values());
330         }
331     }
332 
333     /**
334      * Get a copy of active associations.
335      */
336     @NonNull
getActiveAssociations()337     public List<AssociationInfo> getActiveAssociations() {
338         synchronized (mLock) {
339             return CollectionUtils.filter(getAssociations(), AssociationInfo::isActive);
340         }
341     }
342 
343     /**
344      * Get a copy of all associations by user.
345      */
346     @NonNull
getAssociationsByUser(@serIdInt int userId)347     public List<AssociationInfo> getAssociationsByUser(@UserIdInt int userId) {
348         synchronized (mLock) {
349             return CollectionUtils.filter(getAssociations(), a -> a.getUserId() == userId);
350         }
351     }
352 
353     /**
354      * Get a copy of active associations by user.
355      */
356     @NonNull
getActiveAssociationsByUser(@serIdInt int userId)357     public List<AssociationInfo> getActiveAssociationsByUser(@UserIdInt int userId) {
358         synchronized (mLock) {
359             return CollectionUtils.filter(getActiveAssociations(), a -> a.getUserId() == userId);
360         }
361     }
362 
363     /**
364      * Get a copy of all associations by package.
365      */
366     @NonNull
getAssociationsByPackage(@serIdInt int userId, @NonNull String packageName)367     public List<AssociationInfo> getAssociationsByPackage(@UserIdInt int userId,
368             @NonNull String packageName) {
369         synchronized (mLock) {
370             return CollectionUtils.filter(getAssociationsByUser(userId),
371                     a -> a.getPackageName().equals(packageName));
372         }
373     }
374 
375     /**
376      * Get a copy of active associations by package.
377      */
378     @NonNull
getActiveAssociationsByPackage(@serIdInt int userId, @NonNull String packageName)379     public List<AssociationInfo> getActiveAssociationsByPackage(@UserIdInt int userId,
380             @NonNull String packageName) {
381         synchronized (mLock) {
382             return CollectionUtils.filter(getActiveAssociationsByUser(userId),
383                     a -> a.getPackageName().equals(packageName));
384         }
385     }
386 
387     /**
388      * Get the first active association with the mac address.
389      */
390     @Nullable
getFirstAssociationByAddress( @serIdInt int userId, @NonNull String packageName, @NonNull String macAddress)391     public AssociationInfo getFirstAssociationByAddress(
392             @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
393         synchronized (mLock) {
394             return CollectionUtils.find(getActiveAssociationsByPackage(userId, packageName),
395                     a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress()
396                             .equals(MacAddress.fromString(macAddress)));
397         }
398     }
399 
400     /**
401      * Get the association by id.
402      */
403     @Nullable
getAssociationById(int id)404     public AssociationInfo getAssociationById(int id) {
405         synchronized (mLock) {
406             return mIdToAssociationMap.get(id);
407         }
408     }
409 
410     /**
411      * Get a copy of active associations by mac address.
412      */
413     @NonNull
getActiveAssociationsByAddress(@onNull String macAddress)414     public List<AssociationInfo> getActiveAssociationsByAddress(@NonNull String macAddress) {
415         synchronized (mLock) {
416             return CollectionUtils.filter(getActiveAssociations(),
417                     a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress()
418                             .equals(MacAddress.fromString(macAddress)));
419         }
420     }
421 
422     /**
423      * Get a copy of revoked associations.
424      */
425     @NonNull
getRevokedAssociations()426     public List<AssociationInfo> getRevokedAssociations() {
427         synchronized (mLock) {
428             return CollectionUtils.filter(getAssociations(), AssociationInfo::isRevoked);
429         }
430     }
431 
432     /**
433      * Get a copy of revoked associations for the package.
434      */
435     @NonNull
getRevokedAssociations(@serIdInt int userId, @NonNull String packageName)436     public List<AssociationInfo> getRevokedAssociations(@UserIdInt int userId,
437             @NonNull String packageName) {
438         synchronized (mLock) {
439             return CollectionUtils.filter(getAssociations(),
440                     a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId
441                             && a.isRevoked());
442         }
443     }
444 
445     /**
446      * Get a copy of active associations.
447      */
448     @NonNull
getPendingAssociations(@serIdInt int userId, @NonNull String packageName)449     public List<AssociationInfo> getPendingAssociations(@UserIdInt int userId,
450             @NonNull String packageName) {
451         synchronized (mLock) {
452             return CollectionUtils.filter(getAssociations(),
453                     a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId
454                             && a.isPending());
455         }
456     }
457 
458     /**
459      * Get association by id with caller checks.
460      *
461      * If the association is not found, an IllegalArgumentException would be thrown.
462      *
463      * If the caller can't access the association, a SecurityException would be thrown.
464      */
465     @NonNull
getAssociationWithCallerChecks(int associationId)466     public AssociationInfo getAssociationWithCallerChecks(int associationId) {
467         AssociationInfo association = getAssociationById(associationId);
468         if (association == null) {
469             throw new IllegalArgumentException(
470                     "getAssociationWithCallerChecks() Association id=[" + associationId
471                             + "] doesn't exist.");
472         }
473         enforceCallerCanManageAssociationsForPackage(mContext, association.getUserId(),
474                 association.getPackageName(), null);
475         return association;
476     }
477 
478     /**
479      * Register a local listener for association changes.
480      */
registerLocalListener(@onNull OnChangeListener listener)481     public void registerLocalListener(@NonNull OnChangeListener listener) {
482         synchronized (mLocalListeners) {
483             mLocalListeners.add(listener);
484         }
485     }
486 
487     /**
488      * Unregister a local listener previously registered for association changes.
489      */
unregisterLocalListener(@onNull OnChangeListener listener)490     public void unregisterLocalListener(@NonNull OnChangeListener listener) {
491         synchronized (mLocalListeners) {
492             mLocalListeners.remove(listener);
493         }
494     }
495 
496     /**
497      * Register a remote listener for association changes.
498      */
registerRemoteListener(@onNull IOnAssociationsChangedListener listener, int userId)499     public void registerRemoteListener(@NonNull IOnAssociationsChangedListener listener,
500             int userId) {
501         synchronized (mRemoteListeners) {
502             mRemoteListeners.register(listener, userId);
503         }
504     }
505 
506     /**
507      * Unregister a remote listener previously registered for association changes.
508      */
unregisterRemoteListener(@onNull IOnAssociationsChangedListener listener)509     public void unregisterRemoteListener(@NonNull IOnAssociationsChangedListener listener) {
510         synchronized (mRemoteListeners) {
511             mRemoteListeners.unregister(listener);
512         }
513     }
514 
515     /**
516      * Dumps current companion device association states.
517      */
dump(@onNull PrintWriter out)518     public void dump(@NonNull PrintWriter out) {
519         out.append("Companion Device Associations: ");
520         if (getActiveAssociations().isEmpty()) {
521             out.append("<empty>\n");
522         } else {
523             out.append("\n");
524             for (AssociationInfo a : getActiveAssociations()) {
525                 out.append("  ").append(a.toString()).append('\n');
526             }
527         }
528     }
529 
broadcastChange(@hangeType int changeType, AssociationInfo association)530     private void broadcastChange(@ChangeType int changeType, AssociationInfo association) {
531         Slog.i(TAG, "Broadcasting association changes - changeType=[" + changeType + "]...");
532 
533         synchronized (mLocalListeners) {
534             for (OnChangeListener listener : mLocalListeners) {
535                 listener.onAssociationChanged(changeType, association);
536             }
537         }
538         synchronized (mRemoteListeners) {
539             final int userId = association.getUserId();
540             final List<AssociationInfo> updatedAssociations = getActiveAssociationsByUser(userId);
541             // Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED.
542             // Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in
543             // association's configs, which "listeners" won't (and shouldn't) be able to see.
544             if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) {
545                 mRemoteListeners.broadcast((listener, callbackUserId) -> {
546                     int listenerUserId = (int) callbackUserId;
547                     if (listenerUserId == userId || listenerUserId == UserHandle.USER_ALL) {
548                         try {
549                             listener.onAssociationsChanged(updatedAssociations);
550                         } catch (RemoteException ignored) {
551                         }
552                     }
553                 });
554             }
555         }
556     }
557 }
558