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