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;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SuppressLint;
22 import android.annotation.UserIdInt;
23 import android.companion.AssociationInfo;
24 import android.net.MacAddress;
25 import android.util.Log;
26 import android.util.Slog;
27 import android.util.SparseArray;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.util.CollectionUtils;
31 
32 import java.io.PrintWriter;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.LinkedHashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Objects;
42 import java.util.Set;
43 import java.util.StringJoiner;
44 
45 /**
46  * Implementation of the {@link AssociationStore}, with addition of the methods for modification.
47  * <ul>
48  * <li> {@link #addAssociation(AssociationInfo)}
49  * <li> {@link #removeAssociation(int)}
50  * <li> {@link #updateAssociation(AssociationInfo)}
51  * </ul>
52  *
53  * The class has package-private access level, and instances of the class should only be created by
54  * the {@link CompanionDeviceManagerService}.
55  * Other system component (both inside and outside if the com.android.server.companion package)
56  * should use public {@link AssociationStore} interface.
57  */
58 @SuppressLint("LongLogTag")
59 class AssociationStoreImpl implements AssociationStore {
60     private static final boolean DEBUG = false;
61     private static final String TAG = "CDM_AssociationStore";
62 
63     private final Object mLock = new Object();
64 
65     @GuardedBy("mLock")
66     private final Map<Integer, AssociationInfo> mIdMap = new HashMap<>();
67     @GuardedBy("mLock")
68     private final Map<MacAddress, Set<Integer>> mAddressMap = new HashMap<>();
69     @GuardedBy("mLock")
70     private final SparseArray<List<AssociationInfo>> mCachedPerUser = new SparseArray<>();
71 
72     @GuardedBy("mListeners")
73     private final Set<OnChangeListener> mListeners = new LinkedHashSet<>();
74 
addAssociation(@onNull AssociationInfo association)75     void addAssociation(@NonNull AssociationInfo association) {
76         // Validity check first.
77         checkNotRevoked(association);
78 
79         final int id = association.getId();
80 
81         if (DEBUG) {
82             Log.i(TAG, "addAssociation() " + association.toShortString());
83             Log.d(TAG, "  association=" + association);
84         }
85 
86         synchronized (mLock) {
87             if (mIdMap.containsKey(id)) {
88                 Slog.e(TAG, "Association with id " + id + " already exists.");
89                 return;
90             }
91             mIdMap.put(id, association);
92 
93             final MacAddress address = association.getDeviceMacAddress();
94             if (address != null) {
95                 mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
96             }
97 
98             invalidateCacheForUserLocked(association.getUserId());
99         }
100 
101         broadcastChange(CHANGE_TYPE_ADDED, association);
102     }
103 
updateAssociation(@onNull AssociationInfo updated)104     void updateAssociation(@NonNull AssociationInfo updated) {
105         // Validity check first.
106         checkNotRevoked(updated);
107 
108         final int id = updated.getId();
109 
110         if (DEBUG) {
111             Log.i(TAG, "updateAssociation() " + updated.toShortString());
112             Log.d(TAG, "  updated=" + updated);
113         }
114 
115         final AssociationInfo current;
116         final boolean macAddressChanged;
117         synchronized (mLock) {
118             current = mIdMap.get(id);
119             if (current == null) {
120                 if (DEBUG) Log.w(TAG, "Association with id " + id + " does not exist.");
121                 return;
122             }
123             if (DEBUG) Log.d(TAG, "  current=" + current);
124 
125             if (current.equals(updated)) {
126                 if (DEBUG) Log.w(TAG, "  No changes.");
127                 return;
128             }
129 
130             // Update the ID-to-Association map.
131             mIdMap.put(id, updated);
132             // Invalidate the corresponding user cache entry.
133             invalidateCacheForUserLocked(current.getUserId());
134 
135             // Update the MacAddress-to-List<Association> map if needed.
136             final MacAddress updatedAddress = updated.getDeviceMacAddress();
137             final MacAddress currentAddress = current.getDeviceMacAddress();
138             macAddressChanged = !Objects.equals(currentAddress, updatedAddress);
139             if (macAddressChanged) {
140                 if (currentAddress != null) {
141                     mAddressMap.get(currentAddress).remove(id);
142                 }
143                 if (updatedAddress != null) {
144                     mAddressMap.computeIfAbsent(updatedAddress, it -> new HashSet<>()).add(id);
145                 }
146             }
147         }
148 
149         final int changeType = macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED
150                 : CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
151         broadcastChange(changeType, updated);
152     }
153 
removeAssociation(int id)154     void removeAssociation(int id) {
155         if (DEBUG) Log.i(TAG, "removeAssociation() id=" + id);
156 
157         final AssociationInfo association;
158         synchronized (mLock) {
159             association = mIdMap.remove(id);
160 
161             if (association == null) {
162                 if (DEBUG) Log.w(TAG, "Association with id " + id + " is not stored.");
163                 return;
164             } else {
165                 if (DEBUG) {
166                     Log.i(TAG, "removed " + association.toShortString());
167                     Log.d(TAG, "  association=" + association);
168                 }
169             }
170 
171             final MacAddress macAddress = association.getDeviceMacAddress();
172             if (macAddress != null) {
173                 mAddressMap.get(macAddress).remove(id);
174             }
175 
176             invalidateCacheForUserLocked(association.getUserId());
177         }
178 
179         broadcastChange(CHANGE_TYPE_REMOVED, association);
180     }
181 
182     /**
183      * @return a "snapshot" of the current state of the existing associations.
184      */
getAssociations()185     public @NonNull Collection<AssociationInfo> getAssociations() {
186         synchronized (mLock) {
187             // IMPORTANT: make and return a COPY of the mIdMap.values(), NOT a "direct" reference.
188             // The HashMap.values() returns a collection which is backed by the HashMap, so changes
189             // to the HashMap are reflected in this collection.
190             // For us this means that if mIdMap is modified while the iteration over mIdMap.values()
191             // is in progress it may lead to "undefined results" (according to the HashMap's
192             // documentation) or cause ConcurrentModificationExceptions in the iterator (according
193             // to the bugreports...).
194             return List.copyOf(mIdMap.values());
195         }
196     }
197 
getAssociationsForUser(@serIdInt int userId)198     public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) {
199         synchronized (mLock) {
200             return getAssociationsForUserLocked(userId);
201         }
202     }
203 
getAssociationsForPackage( @serIdInt int userId, @NonNull String packageName)204     public @NonNull List<AssociationInfo> getAssociationsForPackage(
205             @UserIdInt int userId, @NonNull String packageName) {
206         final List<AssociationInfo> associationsForUser = getAssociationsForUser(userId);
207         final List<AssociationInfo> associationsForPackage =
208                 CollectionUtils.filter(associationsForUser,
209                         it -> it.getPackageName().equals(packageName));
210         return Collections.unmodifiableList(associationsForPackage);
211     }
212 
getAssociationsForPackageWithAddress( @serIdInt int userId, @NonNull String packageName, @NonNull String macAddress)213     public @Nullable AssociationInfo getAssociationsForPackageWithAddress(
214             @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
215         final List<AssociationInfo> associations = getAssociationsByAddress(macAddress);
216         return CollectionUtils.find(associations,
217                 it -> it.belongsToPackage(userId, packageName));
218     }
219 
getAssociationById(int id)220     public @Nullable AssociationInfo getAssociationById(int id) {
221         synchronized (mLock) {
222             return mIdMap.get(id);
223         }
224     }
225 
getAssociationsByAddress(@onNull String macAddress)226     public @NonNull List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) {
227         final MacAddress address = MacAddress.fromString(macAddress);
228 
229         synchronized (mLock) {
230             final Set<Integer> ids = mAddressMap.get(address);
231             if (ids == null) return Collections.emptyList();
232 
233             final List<AssociationInfo> associations = new ArrayList<>(ids.size());
234             for (Integer id : ids) {
235                 associations.add(mIdMap.get(id));
236             }
237 
238             return Collections.unmodifiableList(associations);
239         }
240     }
241 
242     @GuardedBy("mLock")
getAssociationsForUserLocked(@serIdInt int userId)243     private @NonNull List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) {
244         final List<AssociationInfo> cached = mCachedPerUser.get(userId);
245         if (cached != null) {
246             return cached;
247         }
248 
249         final List<AssociationInfo> associationsForUser = new ArrayList<>();
250         for (AssociationInfo association : mIdMap.values()) {
251             if (association.getUserId() == userId) {
252                 associationsForUser.add(association);
253             }
254         }
255         final List<AssociationInfo> set = Collections.unmodifiableList(associationsForUser);
256         mCachedPerUser.set(userId, set);
257         return set;
258     }
259 
260     @GuardedBy("mLock")
invalidateCacheForUserLocked(@serIdInt int userId)261     private void invalidateCacheForUserLocked(@UserIdInt int userId) {
262         mCachedPerUser.delete(userId);
263     }
264 
registerListener(@onNull OnChangeListener listener)265     public void registerListener(@NonNull OnChangeListener listener) {
266         synchronized (mListeners) {
267             mListeners.add(listener);
268         }
269     }
270 
unregisterListener(@onNull OnChangeListener listener)271     public void unregisterListener(@NonNull OnChangeListener listener) {
272         synchronized (mListeners) {
273             mListeners.remove(listener);
274         }
275     }
276 
277     /**
278      * Dumps current companion device association states.
279      */
dump(@onNull PrintWriter out)280     public void dump(@NonNull PrintWriter out) {
281         out.append("Companion Device Associations: ");
282         if (getAssociations().isEmpty()) {
283             out.append("<empty>\n");
284         } else {
285             out.append("\n");
286             for (AssociationInfo a : getAssociations()) {
287                 out.append("  ").append(a.toString()).append('\n');
288             }
289         }
290     }
291 
broadcastChange(@hangeType int changeType, AssociationInfo association)292     private void broadcastChange(@ChangeType int changeType, AssociationInfo association) {
293         synchronized (mListeners) {
294             for (OnChangeListener listener : mListeners) {
295                 listener.onAssociationChanged(changeType, association);
296             }
297         }
298     }
299 
setAssociations(Collection<AssociationInfo> allAssociations)300     void setAssociations(Collection<AssociationInfo> allAssociations) {
301         // Validity check first.
302         allAssociations.forEach(AssociationStoreImpl::checkNotRevoked);
303 
304         if (DEBUG) {
305             Log.i(TAG, "setAssociations() n=" + allAssociations.size());
306             final StringJoiner stringJoiner = new StringJoiner(", ");
307             allAssociations.forEach(assoc -> stringJoiner.add(assoc.toShortString()));
308             Log.v(TAG, "  associations=" + stringJoiner);
309         }
310         synchronized (mLock) {
311             setAssociationsLocked(allAssociations);
312         }
313     }
314 
315     @GuardedBy("mLock")
setAssociationsLocked(Collection<AssociationInfo> associations)316     private void setAssociationsLocked(Collection<AssociationInfo> associations) {
317         clearLocked();
318 
319         for (AssociationInfo association : associations) {
320             final int id = association.getId();
321             mIdMap.put(id, association);
322 
323             final MacAddress address = association.getDeviceMacAddress();
324             if (address != null) {
325                 mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id);
326             }
327         }
328     }
329 
330     @GuardedBy("mLock")
clearLocked()331     private void clearLocked() {
332         mIdMap.clear();
333         mAddressMap.clear();
334         mCachedPerUser.clear();
335     }
336 
checkNotRevoked(@onNull AssociationInfo association)337     private static void checkNotRevoked(@NonNull AssociationInfo association) {
338         if (association.isRevoked()) {
339             throw new IllegalArgumentException(
340                     "Revoked (removed) associations MUST NOT appear in the AssociationStore");
341         }
342     }
343 }
344