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