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