diff options
19 files changed, 1318 insertions, 886 deletions
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index b4b96e2c69d6..843158c0e9fb 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -252,14 +252,6 @@ public final class AssociationInfo implements Parcelable { } /** - * @return true if the association is not revoked nor pending - * @hide - */ - public boolean isActive() { - return !mRevoked && !mPending; - } - - /** * @return the last time self reported disconnected for selfManaged only. * @hide */ diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java index 5e52e06248cb..f2409fb8843e 100644 --- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java +++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java @@ -18,8 +18,7 @@ package com.android.server.companion; import static android.os.UserHandle.getCallingUserId; -import static com.android.server.companion.association.AssociationDiskStore.readAssociationsFromPayload; -import static com.android.server.companion.utils.RolesUtils.addRoleHolderForAssociation; +import static com.android.server.companion.CompanionDeviceManagerService.PerUserAssociationSet; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -27,50 +26,62 @@ import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.Flags; import android.companion.datatransfer.SystemDataTransferRequest; -import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManagerInternal; +import android.util.ArraySet; +import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; import com.android.server.companion.association.AssociationDiskStore; import com.android.server.companion.association.AssociationRequestsProcessor; import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.association.Associations; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; @SuppressLint("LongLogTag") class BackupRestoreProcessor { - private static final String TAG = "CDM_BackupRestoreProcessor"; + static final String TAG = "CDM_BackupRestoreProcessor"; private static final int BACKUP_AND_RESTORE_VERSION = 0; - private final Context mContext; @NonNull - private final PackageManagerInternal mPackageManagerInternal; + private final CompanionDeviceManagerService mService; + @NonNull + private final PackageManagerInternal mPackageManager; @NonNull private final AssociationStore mAssociationStore; @NonNull - private final AssociationDiskStore mAssociationDiskStore; + private final AssociationDiskStore mPersistentStore; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull private final AssociationRequestsProcessor mAssociationRequestsProcessor; - BackupRestoreProcessor(@NonNull Context context, - @NonNull PackageManagerInternal packageManagerInternal, + /** + * A structure that consists of a set of restored associations that are pending corresponding + * companion app to be installed. + */ + @GuardedBy("mAssociationsPendingAppInstall") + private final PerUserAssociationSet mAssociationsPendingAppInstall = + new PerUserAssociationSet(); + + BackupRestoreProcessor(@NonNull CompanionDeviceManagerService service, @NonNull AssociationStore associationStore, - @NonNull AssociationDiskStore associationDiskStore, + @NonNull AssociationDiskStore persistentStore, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull AssociationRequestsProcessor associationRequestsProcessor) { - mContext = context; - mPackageManagerInternal = packageManagerInternal; + mService = service; + mPackageManager = service.mPackageManagerInternal; mAssociationStore = associationStore; - mAssociationDiskStore = associationDiskStore; + mPersistentStore = persistentStore; mSystemDataTransferRequestStore = systemDataTransferRequestStore; mAssociationRequestsProcessor = associationRequestsProcessor; } @@ -82,9 +93,9 @@ class BackupRestoreProcessor { * | (4) SystemDataTransferRequest length | SystemDataTransferRequest XML (without userId)| */ byte[] getBackupPayload(int userId) { - Slog.i(TAG, "getBackupPayload() userId=[" + userId + "]."); - - byte[] associationsPayload = mAssociationDiskStore.getBackupPayload(userId); + // Persist state first to generate an up-to-date XML file + mService.persistStateForUser(userId); + byte[] associationsPayload = mPersistentStore.getBackupPayload(userId); int associationsPayloadLength = associationsPayload.length; // System data transfer requests are persisted up-to-date already @@ -108,9 +119,6 @@ class BackupRestoreProcessor { * Create new associations and system data transfer request consents using backed up payload. */ void applyRestoredPayload(byte[] payload, int userId) { - Slog.i(TAG, "applyRestoredPayload() userId=[" + userId + "], payload size=[" - + payload.length + "]."); - ByteBuffer buffer = ByteBuffer.wrap(payload); // Make sure that payload version matches current version to ensure proper deserialization @@ -123,8 +131,9 @@ class BackupRestoreProcessor { // Read the bytes containing backed-up associations byte[] associationsPayload = new byte[buffer.getInt()]; buffer.get(associationsPayload); - final Associations restoredAssociations = readAssociationsFromPayload( - associationsPayload, userId); + final Set<AssociationInfo> restoredAssociations = new HashSet<>(); + mPersistentStore.readStateFromPayload(associationsPayload, userId, + restoredAssociations, new HashMap<>()); // Read the bytes containing backed-up system data transfer requests user consent byte[] requestsPayload = new byte[buffer.getInt()]; @@ -133,13 +142,13 @@ class BackupRestoreProcessor { mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload, userId); // Get a list of installed packages ahead of time. - List<ApplicationInfo> installedApps = mPackageManagerInternal.getInstalledApplications( + List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications( 0, userId, getCallingUserId()); // Restored device may have a different user ID than the backed-up user's user-ID. Since // association ID is dependent on the user ID, restored associations must account for // this potential difference on their association IDs. - for (AssociationInfo restored : restoredAssociations.getAssociations()) { + for (AssociationInfo restored : restoredAssociations) { // Don't restore a revoked association. Since they weren't added to the device being // restored in the first place, there is no need to worry about revoking a role that // was never granted either. @@ -159,9 +168,10 @@ class BackupRestoreProcessor { // Create a new association reassigned to this user and a valid association ID final String packageName = restored.getPackageName(); - final int newId = mAssociationStore.getNextId(userId); - AssociationInfo newAssociation = new AssociationInfo.Builder(newId, userId, packageName, - restored).build(); + final int newId = mService.getNewAssociationIdForPackage(userId, packageName); + AssociationInfo newAssociation = + new AssociationInfo.Builder(newId, userId, packageName, restored) + .build(); // Check if the companion app for this association is already installed, then do one // of the following: @@ -169,15 +179,13 @@ class BackupRestoreProcessor { // the role attached to this association to the app. // (2) If the app isn't yet installed, then add this association to the list of pending // associations to be added when the package is installed in the future. - boolean isPackageInstalled = installedApps.stream().anyMatch( - app -> packageName.equals(app.packageName)); + boolean isPackageInstalled = installedApps.stream() + .anyMatch(app -> packageName.equals(app.packageName)); if (isPackageInstalled) { mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, null, null); } else { - newAssociation = (new AssociationInfo.Builder(newAssociation)).setPending(true) - .build(); - mAssociationStore.addAssociation(newAssociation); + addToPendingAppInstall(newAssociation); } // Re-map restored system data transfer requests to newly created associations @@ -187,27 +195,32 @@ class BackupRestoreProcessor { mSystemDataTransferRequestStore.writeRequest(userId, newRequest); } } + + // Persist restored state. + mService.persistStateForUser(userId); + } + + void addToPendingAppInstall(@NonNull AssociationInfo association) { + association = (new AssociationInfo.Builder(association)) + .setPending(true) + .build(); + + synchronized (mAssociationsPendingAppInstall) { + mAssociationsPendingAppInstall.forUser(association.getUserId()).add(association); + } } - public void restorePendingAssociations(int userId, String packageName) { - List<AssociationInfo> pendingAssociations = mAssociationStore.getPendingAssociations(userId, - packageName); - if (!pendingAssociations.isEmpty()) { - Slog.i(TAG, "Found pending associations for package=[" + packageName - + "]. Restoring..."); + void removeFromPendingAppInstall(@NonNull AssociationInfo association) { + synchronized (mAssociationsPendingAppInstall) { + mAssociationsPendingAppInstall.forUser(association.getUserId()).remove(association); } - for (AssociationInfo association : pendingAssociations) { - AssociationInfo newAssociation = new AssociationInfo.Builder(association) - .setPending(false) - .build(); - addRoleHolderForAssociation(mContext, newAssociation, success -> { - if (success) { - mAssociationStore.updateAssociation(newAssociation); - Slog.i(TAG, "Association=[" + association + "] is restored."); - } else { - Slog.e(TAG, "Failed to restore association=[" + association + "]."); - } - }); + } + + @NonNull + Set<AssociationInfo> getAssociationsPendingAppInstallForUser(@UserIdInt int userId) { + synchronized (mAssociationsPendingAppInstall) { + // Return a copy. + return new ArraySet<>(mAssociationsPendingAppInstall.forUser(userId)); } } @@ -218,7 +231,7 @@ class BackupRestoreProcessor { private boolean handleCollision(@UserIdInt int userId, AssociationInfo restored, List<SystemDataTransferRequest> restoredRequests) { - List<AssociationInfo> localAssociations = mAssociationStore.getActiveAssociationsByPackage( + List<AssociationInfo> localAssociations = mAssociationStore.getAssociationsForPackage( restored.getUserId(), restored.getPackageName()); Predicate<AssociationInfo> isSameDevice = associationInfo -> { boolean matchesMacAddress = Objects.equals( @@ -235,7 +248,7 @@ class BackupRestoreProcessor { return false; } - Slog.d(TAG, "Conflict detected with association id=" + local.getId() + Log.d(TAG, "Conflict detected with association id=" + local.getId() + " while restoring CDM backup. Keeping local association."); List<SystemDataTransferRequest> localRequests = mSystemDataTransferRequestStore @@ -253,8 +266,8 @@ class BackupRestoreProcessor { continue; } - Slog.d(TAG, "Restoring " + restoredRequest.getClass().getSimpleName() - + " to an existing association id=[" + local.getId() + "]."); + Log.d(TAG, "Restoring " + restoredRequest.getClass().getSimpleName() + + " to an existing association id=" + local.getId() + "."); SystemDataTransferRequest newRequest = restoredRequest.copyWithNewId(local.getId()); diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java index 0a4148535451..c801489ce963 100644 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -397,7 +397,7 @@ public class CompanionApplicationController { // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. if (isPrimary) { final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + mAssociationStore.getAssociationsForPackage(userId, packageName); for (AssociationInfo association : associations) { final String deviceProfile = association.getDeviceProfile(); @@ -442,7 +442,7 @@ public class CompanionApplicationController { mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); for (AssociationInfo ai : - mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { + mAssociationStore.getAssociationsForPackage(userId, packageName)) { final int associationId = ai.getId(); stillAssociated = true; if (ai.isSelfManaged()) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 73ebbc781c74..3846e981cbab 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -37,9 +37,12 @@ import static android.os.UserHandle.getCallingUserId; import static com.android.internal.util.CollectionUtils.any; import static com.android.internal.util.Preconditions.checkState; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.server.companion.association.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED; +import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser; +import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser; +import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PackageUtils.getPackageInfo; -import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid; @@ -79,16 +82,20 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.UserInfo; import android.hardware.power.Mode; import android.net.MacAddress; import android.net.NetworkPolicyManager; import android.os.Binder; import android.os.Environment; +import android.os.Handler; +import android.os.Message; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.ParcelUuid; -import android.os.PowerExemptionManager; import android.os.PowerManagerInternal; +import android.os.PowerWhitelistManager; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; @@ -98,9 +105,13 @@ import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; import com.android.internal.content.PackageMonitor; +import com.android.internal.infra.PerUser; import com.android.internal.notification.NotificationAccessConfirmationActivityContract; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; @@ -110,8 +121,8 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.association.AssociationDiskStore; import com.android.server.companion.association.AssociationRequestsProcessor; +import com.android.server.companion.association.AssociationRevokeProcessor; import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.association.DisassociationProcessor; import com.android.server.companion.association.InactiveAssociationsRemovalService; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; @@ -128,6 +139,7 @@ import com.android.server.wm.ActivityTaskManagerInternal; import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -152,51 +164,80 @@ public class CompanionDeviceManagerService extends SystemService { private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); private static final int MAX_CN_LENGTH = 500; + private final ActivityManager mActivityManager; + private AssociationDiskStore mAssociationDiskStore; + private final PersistUserStateHandler mUserPersistenceHandler; + + private final AssociationStore mAssociationStore; + private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; + private AssociationRequestsProcessor mAssociationRequestsProcessor; + private SystemDataTransferProcessor mSystemDataTransferProcessor; + private BackupRestoreProcessor mBackupRestoreProcessor; + private CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private CompanionApplicationController mCompanionAppController; + private CompanionTransportManager mTransportManager; + private AssociationRevokeProcessor mAssociationRevokeProcessor; + private final ActivityTaskManagerInternal mAtmInternal; private final ActivityManagerInternal mAmInternal; private final IAppOpsService mAppOpsManager; - private final PowerExemptionManager mPowerExemptionManager; - private final PackageManagerInternal mPackageManagerInternal; + private final PowerWhitelistManager mPowerWhitelistManager; + private final UserManager mUserManager; + public final PackageManagerInternal mPackageManagerInternal; private final PowerManagerInternal mPowerManagerInternal; - private final AssociationStore mAssociationStore; - private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; - private final ObservableUuidStore mObservableUuidStore; - private final AssociationRequestsProcessor mAssociationRequestsProcessor; - private final SystemDataTransferProcessor mSystemDataTransferProcessor; - private final BackupRestoreProcessor mBackupRestoreProcessor; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final CompanionApplicationController mCompanionAppController; - private final CompanionTransportManager mTransportManager; - private final DisassociationProcessor mDisassociationProcessor; - private final CrossDeviceSyncController mCrossDeviceSyncController; + /** + * A structure that consists of two nested maps, and effectively maps (userId + packageName) to + * a list of IDs that have been previously assigned to associations for that package. + * We maintain this structure so that we never re-use association IDs for the same package + * (until it's uninstalled). + */ + @GuardedBy("mPreviouslyUsedIds") + private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>(); + + private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners = + new RemoteCallbackList<>(); + + private CrossDeviceSyncController mCrossDeviceSyncController; + + private ObservableUuidStore mObservableUuidStore; public CompanionDeviceManagerService(Context context) { super(context); - final ActivityManager activityManager = context.getSystemService(ActivityManager.class); - mPowerExemptionManager = context.getSystemService(PowerExemptionManager.class); + mActivityManager = context.getSystemService(ActivityManager.class); + mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class); mAppOpsManager = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mAmInternal = LocalServices.getService(ActivityManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); - final UserManager userManager = context.getSystemService(UserManager.class); - mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); + mUserManager = context.getSystemService(UserManager.class); - final AssociationDiskStore associationDiskStore = new AssociationDiskStore(); - mAssociationStore = new AssociationStore(userManager, associationDiskStore); + mUserPersistenceHandler = new PersistUserStateHandler(); + mAssociationStore = new AssociationStore(); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); + + mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); mObservableUuidStore = new ObservableUuidStore(); + } + + @Override + public void onStart() { + final Context context = getContext(); - // Init processors - mAssociationRequestsProcessor = new AssociationRequestsProcessor(context, - mPackageManagerInternal, mAssociationStore); - mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal, - mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, - mAssociationRequestsProcessor); + mAssociationDiskStore = new AssociationDiskStore(); + mAssociationRequestsProcessor = new AssociationRequestsProcessor( + /* cdmService */ this, mAssociationStore); + mBackupRestoreProcessor = new BackupRestoreProcessor( + /* cdmService */ this, mAssociationStore, mAssociationDiskStore, + mSystemDataTransferRequestStore, mAssociationRequestsProcessor); + + mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); - mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager, + mAssociationStore.registerListener(mAssociationStoreChangeListener); + + mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager, mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); mCompanionAppController = new CompanionApplicationController( @@ -205,9 +246,11 @@ public class CompanionDeviceManagerService extends SystemService { mTransportManager = new CompanionTransportManager(context, mAssociationStore); - mDisassociationProcessor = new DisassociationProcessor(context, activityManager, - mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor, - mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager); + mAssociationRevokeProcessor = new AssociationRevokeProcessor(this, mAssociationStore, + mPackageManagerInternal, mDevicePresenceMonitor, mCompanionAppController, + mSystemDataTransferRequestStore, mTransportManager); + + loadAssociationsFromDisk(); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, @@ -215,16 +258,6 @@ public class CompanionDeviceManagerService extends SystemService { // TODO(b/279663946): move context sync to a dedicated system service mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); - } - - @Override - public void onStart() { - // Init association stores - mAssociationStore.refreshCache(); - mAssociationStore.registerLocalListener(mAssociationStoreChangeListener); - - // Init UUID store - mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); // Publish "binder" service. final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl(); @@ -234,6 +267,50 @@ public class CompanionDeviceManagerService extends SystemService { LocalServices.addService(CompanionDeviceManagerServiceInternal.class, new LocalService()); } + void loadAssociationsFromDisk() { + final Set<AssociationInfo> allAssociations = new ArraySet<>(); + synchronized (mPreviouslyUsedIds) { + List<Integer> userIds = new ArrayList<>(); + for (UserInfo user : mUserManager.getAliveUsers()) { + userIds.add(user.id); + } + // The data is stored in DE directories, so we can read the data for all users now + // (which would not be possible if the data was stored to CE directories). + mAssociationDiskStore.readStateForUsers(userIds, allAssociations, mPreviouslyUsedIds); + } + + final Set<AssociationInfo> activeAssociations = + new ArraySet<>(/* capacity */ allAssociations.size()); + // A set contains the userIds that need to persist state after remove the app + // from the list of role holders. + final Set<Integer> usersToPersistStateFor = new ArraySet<>(); + + for (AssociationInfo association : allAssociations) { + if (association.isPending()) { + mBackupRestoreProcessor.addToPendingAppInstall(association); + } else if (!association.isRevoked()) { + activeAssociations.add(association); + } else if (mAssociationRevokeProcessor.maybeRemoveRoleHolderForAssociation( + association)) { + // Nothing more to do here, but we'll need to persist all the associations to the + // disk afterwards. + usersToPersistStateFor.add(association.getUserId()); + } else { + mAssociationRevokeProcessor.addToPendingRoleHolderRemoval(association); + } + } + + mAssociationStore.setAssociationsToCache(activeAssociations); + + // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because + // persistStateForUser() queries AssociationStore. + // (If persistStateForUser() is invoked before mAssociationStore.setAssociations() it + // would effectively just clear-out all the persisted associations). + for (int userId : usersToPersistStateFor) { + persistStateForUser(userId); + } + } + @Override public void onBootPhase(int phase) { final Context context = getContext(); @@ -252,10 +329,8 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void onUserUnlocking(@NonNull TargetUser user) { - Slog.d(TAG, "onUserUnlocking..."); final int userId = user.getUserIdentifier(); - final List<AssociationInfo> associations = mAssociationStore.getActiveAssociationsByUser( - userId); + final List<AssociationInfo> associations = mAssociationStore.getAssociationsForUser(userId); if (associations.isEmpty()) return; @@ -284,8 +359,7 @@ public class CompanionDeviceManagerService extends SystemService { ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids); for (AssociationInfo ai : - mAssociationStore.getActiveAssociationsByAddress( - bluetoothDevice.getAddress())) { + mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) { Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected"); mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId()); } @@ -305,7 +379,7 @@ public class CompanionDeviceManagerService extends SystemService { @NonNull AssociationInfo getAssociationWithCallerChecks( @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( + AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress( userId, packageName, macAddress); association = sanitizeWithCallerChecks(getContext(), association); if (association != null) { @@ -459,7 +533,7 @@ public class CompanionDeviceManagerService extends SystemService { */ private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { final List<AssociationInfo> packageAssociations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + mAssociationStore.getAssociationsForPackage(userId, packageName); final List<ObservableUuid> observableUuids = mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); @@ -477,6 +551,77 @@ public class CompanionDeviceManagerService extends SystemService { return false; } + private void onAssociationChangedInternal( + @AssociationStore.ChangeType int changeType, AssociationInfo association) { + final int id = association.getId(); + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + + if (changeType == AssociationStore.CHANGE_TYPE_REMOVED) { + markIdAsPreviouslyUsedForPackage(id, userId, packageName); + } + + final List<AssociationInfo> updatedAssociations = + mAssociationStore.getAssociationsForUser(userId); + + mUserPersistenceHandler.postPersistUserState(userId); + + // Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED. + // Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in association's + // configs, which "listeners" won't (and shouldn't) be able to see. + if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) { + notifyListeners(userId, updatedAssociations); + } + updateAtm(userId, updatedAssociations); + } + + void persistStateForUser(@UserIdInt int userId) { + // We want to store both active associations and the revoked (removed) association that we + // are keeping around for the final clean-up (delayed role holder removal). + final List<AssociationInfo> allAssociations; + // Start with the active associations - these we can get from the AssociationStore. + allAssociations = new ArrayList<>( + mAssociationStore.getAssociationsForUser(userId)); + // ... and add the revoked (removed) association, that are yet to be permanently removed. + allAssociations.addAll( + mAssociationRevokeProcessor.getPendingRoleHolderRemovalAssociationsForUser(userId)); + // ... and add the restored associations that are pending missing package installation. + allAssociations.addAll(mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId)); + + final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId); + + mAssociationDiskStore.persistStateForUser(userId, allAssociations, usedIdsForUser); + } + + private void notifyListeners( + @UserIdInt int userId, @NonNull List<AssociationInfo> associations) { + mListeners.broadcast((listener, callbackUserId) -> { + int listenerUserId = (int) callbackUserId; + if (listenerUserId == userId || listenerUserId == UserHandle.USER_ALL) { + try { + listener.onAssociationsChanged(associations); + } catch (RemoteException ignored) { + } + } + }); + } + + private void markIdAsPreviouslyUsedForPackage( + int associationId, @UserIdInt int userId, @NonNull String packageName) { + synchronized (mPreviouslyUsedIds) { + Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId); + if (usedIdsForUser == null) { + usedIdsForUser = new HashMap<>(); + mPreviouslyUsedIds.put(userId, usedIdsForUser); + } + + final Set<Integer> usedIdsForPackage = + usedIdsForUser.computeIfAbsent(packageName, it -> new HashSet<>()); + usedIdsForPackage.add(associationId); + } + } + private void onPackageRemoveOrDataClearedInternal( @UserIdInt int userId, @NonNull String packageName) { if (DEBUG) { @@ -484,20 +629,19 @@ public class CompanionDeviceManagerService extends SystemService { + packageName); } - // Clear all associations for the package. + // Clear associations. final List<AssociationInfo> associationsForPackage = - mAssociationStore.getAssociationsByPackage(userId, packageName); - if (!associationsForPackage.isEmpty()) { - Slog.i(TAG, "Package removed or data cleared for user=[" + userId + "], package=[" - + packageName + "]. Cleaning up CDM data..."); + mAssociationStore.getAssociationsForPackage(userId, packageName); + final List<ObservableUuid> uuidsTobeObserved = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + for (AssociationInfo association : associationsForPackage) { + mAssociationStore.removeAssociation(association.getId()); } + // Clear role holders for (AssociationInfo association : associationsForPackage) { - mDisassociationProcessor.disassociate(association.getId()); + mAssociationRevokeProcessor.maybeRemoveRoleHolderForAssociation(association); } - - // Clear observable UUIDs for the package. - final List<ObservableUuid> uuidsTobeObserved = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + // Clear the uuids to be observed. for (ObservableUuid uuid : uuidsTobeObserved) { mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName); } @@ -508,13 +652,31 @@ public class CompanionDeviceManagerService extends SystemService { private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) { if (DEBUG) Log.i(TAG, "onPackageModified() u" + userId + "/" + packageName); - updateSpecialAccessPermissionForAssociatedPackage(userId, packageName); + final List<AssociationInfo> associationsForPackage = + mAssociationStore.getAssociationsForPackage(userId, packageName); + for (AssociationInfo association : associationsForPackage) { + updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); + } mCompanionAppController.onPackagesChanged(userId); } private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { - mBackupRestoreProcessor.restorePendingAssociations(userId, packageName); + if (DEBUG) Log.i(TAG, "onPackageAddedInternal() u" + userId + "/" + packageName); + + Set<AssociationInfo> associationsPendingAppInstall = mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId); + for (AssociationInfo association : associationsPendingAppInstall) { + if (!packageName.equals(association.getPackageName())) continue; + + AssociationInfo newAssociation = new AssociationInfo.Builder(association) + .setPending(false) + .build(); + mAssociationRequestsProcessor.maybeGrantRoleAndStoreAssociation(newAssociation, + null, null); + mBackupRestoreProcessor.removeFromPendingAppInstall(association); + } } // Revoke associations if the selfManaged companion device does not connect for 3 months. @@ -536,7 +698,7 @@ public class CompanionDeviceManagerService extends SystemService { final int id = association.getId(); Slog.i(TAG, "Removing inactive self-managed association id=" + id); - mDisassociationProcessor.disassociate(id); + mAssociationRevokeProcessor.disassociateInternal(id); } } @@ -588,7 +750,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceUsesCompanionDeviceFeature(getContext(), userId, packageName); } - return mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + return mAssociationStore.getAssociationsForPackage(userId, packageName); } @Override @@ -599,9 +761,9 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId); if (userId == UserHandle.USER_ALL) { - return mAssociationStore.getActiveAssociations(); + return List.copyOf(mAssociationStore.getAssociations()); } - return mAssociationStore.getActiveAssociationsByUser(userId); + return mAssociationStore.getAssociationsForUser(userId); } @Override @@ -611,8 +773,7 @@ public class CompanionDeviceManagerService extends SystemService { addOnAssociationsChangedListener_enforcePermission(); enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId); - - mAssociationStore.registerRemoteListener(listener, userId); + mListeners.register(listener, userId); } @Override @@ -623,7 +784,7 @@ public class CompanionDeviceManagerService extends SystemService { enforceCallerIsSystemOrCanInteractWithUserId(getContext(), userId); - mAssociationStore.unregisterRemoteListener(listener); + mListeners.unregister(listener); } @Override @@ -682,16 +843,16 @@ public class CompanionDeviceManagerService extends SystemService { final AssociationInfo association = getAssociationWithCallerChecks(userId, packageName, deviceMacAddress); - mDisassociationProcessor.disassociate(association.getId()); + mAssociationRevokeProcessor.disassociateInternal(association.getId()); } @Override public void disassociate(int associationId) { - Slog.i(TAG, "disassociate() associationId=" + associationId); + Log.i(TAG, "disassociate() associationId=" + associationId); final AssociationInfo association = getAssociationWithCallerChecks(associationId); - mDisassociationProcessor.disassociate(association.getId()); + mAssociationRevokeProcessor.disassociateInternal(association.getId()); } @Override @@ -706,7 +867,8 @@ public class CompanionDeviceManagerService extends SystemService { throw new IllegalArgumentException("Component name is too long."); } - return Binder.withCleanCallingIdentity(() -> { + final long identity = Binder.clearCallingIdentity(); + try { if (!isRestrictedSettingsAllowed(getContext(), callingPackage, callingUid)) { Slog.e(TAG, "Side loaded app must enable restricted " + "setting before request the notification access"); @@ -720,7 +882,9 @@ public class CompanionDeviceManagerService extends SystemService { | PendingIntent.FLAG_CANCEL_CURRENT, null /* options */, new UserHandle(userId)); - }); + } finally { + Binder.restoreCallingIdentity(identity); + } } /** @@ -748,7 +912,7 @@ public class CompanionDeviceManagerService extends SystemService { return true; } - return any(mAssociationStore.getActiveAssociationsByPackage(userId, packageName), + return any(mAssociationStore.getAssociationsForPackage(userId, packageName), a -> a.isLinkedTo(macAddress)); } @@ -1002,7 +1166,7 @@ public class CompanionDeviceManagerService extends SystemService { final int userId = getCallingUserId(); enforceCallerIsSystemOr(userId, packageName); - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( + AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress( userId, packageName, deviceAddress); if (association == null) { @@ -1075,15 +1239,14 @@ public class CompanionDeviceManagerService extends SystemService { enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage); checkState(!ArrayUtils.isEmpty( - mAssociationStore.getActiveAssociationsByPackage(userId, - callingPackage)), + mAssociationStore.getAssociationsForPackage(userId, callingPackage)), "App must have an association before calling this API"); } @Override public boolean canPairWithoutPrompt(String packageName, String macAddress, int userId) { final AssociationInfo association = - mAssociationStore.getFirstAssociationByAddress( + mAssociationStore.getAssociationsForPackageWithAddress( userId, packageName, macAddress); if (association == null) { return false; @@ -1106,11 +1269,13 @@ public class CompanionDeviceManagerService extends SystemService { @Override public byte[] getBackupPayload(int userId) { + Log.i(TAG, "getBackupPayload() userId=" + userId); return mBackupRestoreProcessor.getBackupPayload(userId); } @Override public void applyRestoredPayload(byte[] payload, int userId) { + Log.i(TAG, "applyRestoredPayload() userId=" + userId); mBackupRestoreProcessor.applyRestoredPayload(payload, userId); } @@ -1121,7 +1286,7 @@ public class CompanionDeviceManagerService extends SystemService { return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this, mAssociationStore, mDevicePresenceMonitor, mTransportManager, mSystemDataTransferProcessor, mAssociationRequestsProcessor, - mBackupRestoreProcessor, mDisassociationProcessor) + mBackupRestoreProcessor, mAssociationRevokeProcessor) .exec(this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); } @@ -1149,6 +1314,88 @@ public class CompanionDeviceManagerService extends SystemService { /* callback */ null, /* resultReceiver */ null); } + @NonNull + private Map<String, Set<Integer>> getPreviouslyUsedIdsForUser(@UserIdInt int userId) { + synchronized (mPreviouslyUsedIds) { + return getPreviouslyUsedIdsForUserLocked(userId); + } + } + + @GuardedBy("mPreviouslyUsedIds") + @NonNull + private Map<String, Set<Integer>> getPreviouslyUsedIdsForUserLocked(@UserIdInt int userId) { + final Map<String, Set<Integer>> usedIdsForUser = mPreviouslyUsedIds.get(userId); + if (usedIdsForUser == null) { + return Collections.emptyMap(); + } + return deepUnmodifiableCopy(usedIdsForUser); + } + + @GuardedBy("mPreviouslyUsedIds") + @NonNull + private Set<Integer> getPreviouslyUsedIdsForPackageLocked( + @UserIdInt int userId, @NonNull String packageName) { + // "Deeply unmodifiable" map: the map itself and the Set<Integer> values it contains are all + // unmodifiable. + final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUserLocked(userId); + final Set<Integer> usedIdsForPackage = usedIdsForUser.get(packageName); + + if (usedIdsForPackage == null) { + return Collections.emptySet(); + } + + //The set is already unmodifiable. + return usedIdsForPackage; + } + + /** + * Get a new association id for the package. + */ + public int getNewAssociationIdForPackage(@UserIdInt int userId, @NonNull String packageName) { + synchronized (mPreviouslyUsedIds) { + // First: collect all IDs currently in use for this user's Associations. + final SparseBooleanArray usedIds = new SparseBooleanArray(); + + // We should really only be checking associations for the given user (i.e.: + // mAssociationStore.getAssociationsForUser(userId)), BUT in the past we've got in a + // state where association IDs were not assigned correctly in regard to + // user-to-association-ids-range (e.g. associations with IDs from 1 to 100,000 should + // always belong to u0), so let's check all the associations. + for (AssociationInfo it : mAssociationStore.getAssociations()) { + usedIds.put(it.getId(), true); + } + + // Some IDs may be reserved by associations that aren't stored yet due to missing + // package after a backup restoration. We don't want the ID to have been taken by + // another association by the time when it is activated from the package installation. + final Set<AssociationInfo> pendingAssociations = mBackupRestoreProcessor + .getAssociationsPendingAppInstallForUser(userId); + for (AssociationInfo it : pendingAssociations) { + usedIds.put(it.getId(), true); + } + + // Second: collect all IDs that have been previously used for this package (and user). + final Set<Integer> previouslyUsedIds = + getPreviouslyUsedIdsForPackageLocked(userId, packageName); + + int id = getFirstAssociationIdForUser(userId); + final int lastAvailableIdForUser = getLastAssociationIdForUser(userId); + + // Find first ID that isn't used now AND has never been used for the given package. + while (usedIds.get(id) || previouslyUsedIds.contains(id)) { + // Increment and try again + id++; + // ... but first check if the ID is valid (within the range allocated to the user). + if (id > lastAvailableIdForUser) { + throw new RuntimeException("Cannot create a new Association ID for " + + packageName + " for user " + userId); + } + } + + return id; + } + } + /** * Update special access for the association's package */ @@ -1156,27 +1403,20 @@ public class CompanionDeviceManagerService extends SystemService { final PackageInfo packageInfo = getPackageInfo(getContext(), userId, packageName); - Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo, - userId, packageName)); + Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo)); } - private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo, int userId, - String packageName) { + private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) { if (packageInfo == null) { return; } - - List<AssociationInfo> associations = mAssociationStore.getActiveAssociationsByPackage( - userId, packageName); - if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, - android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND) - && !associations.isEmpty()) { - mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName); + android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { + mPowerWhitelistManager.addToWhitelist(packageInfo.packageName); } else { try { - mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName); + mPowerWhitelistManager.removeFromWhitelist(packageInfo.packageName); } catch (UnsupportedOperationException e) { Slog.w(TAG, packageInfo.packageName + " can't be removed from power save" + " whitelist. It might due to the package is whitelisted by the system."); @@ -1187,8 +1427,7 @@ public class CompanionDeviceManagerService extends SystemService { try { if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.USE_DATA_IN_BACKGROUND, - android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND) - && !associations.isEmpty()) { + android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) { networkPolicyManager.addUidPolicy( packageInfo.applicationInfo.uid, NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); @@ -1248,7 +1487,7 @@ public class CompanionDeviceManagerService extends SystemService { try { final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByUser(userId); + mAssociationStore.getAssociationsForUser(userId); for (AssociationInfo a : associations) { try { int uid = pm.getPackageUidAsUser(a.getPackageName(), userId); @@ -1267,16 +1506,7 @@ public class CompanionDeviceManagerService extends SystemService { new AssociationStore.OnChangeListener() { @Override public void onAssociationChanged(int changeType, AssociationInfo association) { - Slog.d(TAG, "onAssociationChanged changeType=[" + changeType - + "], association=[" + association); - - final int userId = association.getUserId(); - final List<AssociationInfo> updatedAssociations = - mAssociationStore.getActiveAssociationsByUser(userId); - - updateAtm(userId, updatedAssociations); - updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), - association.getPackageName()); + onAssociationChangedInternal(changeType, association); } }; @@ -1404,4 +1634,64 @@ public class CompanionDeviceManagerService extends SystemService { } } } + + /** + * This method must only be called from {@link CompanionDeviceShellCommand} for testing + * purposes only! + */ + void persistState() { + mUserPersistenceHandler.clearMessages(); + for (UserInfo user : mUserManager.getAliveUsers()) { + persistStateForUser(user.id); + } + } + + /** + * This class is dedicated to handling requests to persist user state. + */ + @SuppressLint("HandlerLeak") + private class PersistUserStateHandler extends Handler { + PersistUserStateHandler() { + super(BackgroundThread.get().getLooper()); + } + + /** + * Persists user state unless there is already an outstanding request for the given user. + */ + synchronized void postPersistUserState(@UserIdInt int userId) { + if (!hasMessages(userId)) { + sendMessage(obtainMessage(userId)); + } + } + + /** + * Clears *ALL* outstanding persist requests for *ALL* users. + */ + synchronized void clearMessages() { + removeCallbacksAndMessages(null); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int userId = msg.what; + persistStateForUser(userId); + } + } + + /** + * Persist associations + */ + public void postPersistUserState(@UserIdInt int userId) { + mUserPersistenceHandler.postPersistUserState(userId); + } + + /** + * Set to store associations + */ + public static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> { + @Override + protected @NonNull Set<AssociationInfo> create(int userId) { + return new ArraySet<>(); + } + } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index a7a73cb6bddb..16877dcaf183 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -33,8 +33,8 @@ import android.util.Base64; import android.util.proto.ProtoOutputStream; import com.android.server.companion.association.AssociationRequestsProcessor; +import com.android.server.companion.association.AssociationRevokeProcessor; import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.association.DisassociationProcessor; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; @@ -49,7 +49,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private static final String TAG = "CDM_CompanionDeviceShellCommand"; private final CompanionDeviceManagerService mService; - private final DisassociationProcessor mDisassociationProcessor; + private final AssociationRevokeProcessor mRevokeProcessor; private final AssociationStore mAssociationStore; private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; private final CompanionTransportManager mTransportManager; @@ -65,7 +65,7 @@ class CompanionDeviceShellCommand extends ShellCommand { SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor, BackupRestoreProcessor backupRestoreProcessor, - DisassociationProcessor disassociationProcessor) { + AssociationRevokeProcessor revokeProcessor) { mService = service; mAssociationStore = associationStore; mDevicePresenceMonitor = devicePresenceMonitor; @@ -73,7 +73,7 @@ class CompanionDeviceShellCommand extends ShellCommand { mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; mBackupRestoreProcessor = backupRestoreProcessor; - mDisassociationProcessor = disassociationProcessor; + mRevokeProcessor = revokeProcessor; } @Override @@ -105,15 +105,12 @@ class CompanionDeviceShellCommand extends ShellCommand { case "list": { final int userId = getNextIntArgRequired(); final List<AssociationInfo> associationsForUser = - mAssociationStore.getActiveAssociationsByUser(userId); - final int maxId = mAssociationStore.getMaxId(userId); - out.println("Max ID: " + maxId); - out.println("Association ID | Package Name | Mac Address"); + mAssociationStore.getAssociationsForUser(userId); for (AssociationInfo association : associationsForUser) { // TODO(b/212535524): use AssociationInfo.toShortString(), once it's not // longer referenced in tests. - out.println(association.getId() + " | " + association.getPackageName() - + " | " + association.getDeviceMacAddress()); + out.println(association.getPackageName() + " " + + association.getDeviceMacAddress() + " " + association.getId()); } } break; @@ -135,24 +132,28 @@ class CompanionDeviceShellCommand extends ShellCommand { final String address = getNextArgRequired(); final AssociationInfo association = mService.getAssociationWithCallerChecks(userId, packageName, address); - mDisassociationProcessor.disassociate(association.getId()); + if (association != null) { + mRevokeProcessor.disassociateInternal(association.getId()); + } } break; case "disassociate-all": { final int userId = getNextIntArgRequired(); + final String packageName = getNextArgRequired(); final List<AssociationInfo> userAssociations = - mAssociationStore.getAssociationsByUser(userId); + mAssociationStore.getAssociationsForPackage(userId, packageName); for (AssociationInfo association : userAssociations) { if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { - mDisassociationProcessor.disassociate(association.getId()); + mRevokeProcessor.disassociateInternal(association.getId()); } } } break; - case "refresh-cache": - mAssociationStore.refreshCache(); + case "clear-association-memory-cache": + mService.persistState(); + mService.loadAssociationsFromDisk(); break; case "simulate-device-appeared": diff --git a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index 46d60f9c8504..75cb12058247 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -16,6 +16,7 @@ package com.android.server.companion.association; +import static com.android.internal.util.CollectionUtils.forEach; import static com.android.internal.util.XmlUtils.readBooleanAttribute; import static com.android.internal.util.XmlUtils.readIntAttribute; import static com.android.internal.util.XmlUtils.readLongAttribute; @@ -25,6 +26,7 @@ import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.internal.util.XmlUtils.writeLongAttribute; import static com.android.internal.util.XmlUtils.writeStringAttribute; import static com.android.server.companion.utils.AssociationUtils.getFirstAssociationIdForUser; +import static com.android.server.companion.utils.AssociationUtils.getLastAssociationIdForUser; import static com.android.server.companion.utils.DataStoreUtils.createStorageFileForUser; import static com.android.server.companion.utils.DataStoreUtils.fileToByteArray; import static com.android.server.companion.utils.DataStoreUtils.isEndOfTag; @@ -38,8 +40,10 @@ import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.net.MacAddress; import android.os.Environment; +import android.util.ArrayMap; import android.util.AtomicFile; import android.util.Slog; +import android.util.SparseArray; import android.util.Xml; import com.android.internal.util.XmlUtils; @@ -55,9 +59,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -76,8 +82,8 @@ import java.util.concurrent.ConcurrentMap; * <p> * Before Android T the data was stored using the v0 schema. See: * <ul> - * <li>{@link #readAssociationsV0(TypedXmlPullParser, int) readAssociationsV0()}. - * <li>{@link #readAssociationV0(TypedXmlPullParser, int, int) readAssociationV0()}. + * <li>{@link #readAssociationsV0(TypedXmlPullParser, int, Collection) readAssociationsV0()}. + * <li>{@link #readAssociationV0(TypedXmlPullParser, int, int, Collection) readAssociationV0()}. * </ul> * * The following snippet is a sample of a file that is using v0 schema. @@ -110,14 +116,15 @@ import java.util.concurrent.ConcurrentMap; * optional. * <ul> * <li> {@link #CURRENT_PERSISTENCE_VERSION} - * <li> {@link #readAssociationsV1(TypedXmlPullParser, int) readAssociationsV1()} - * <li> {@link #readAssociationV1(TypedXmlPullParser, int) readAssociationV1()} + * <li> {@link #readAssociationsV1(TypedXmlPullParser, int, Collection) readAssociationsV1()} + * <li> {@link #readAssociationV1(TypedXmlPullParser, int, Collection) readAssociationV1()} + * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()} * </ul> * * The following snippet is a sample of a file that is using v1 schema. * <pre>{@code * <state persistence-version="1"> - * <associations max-id="3"> + * <associations> * <association * id="1" * package="com.sample.companion.app" @@ -141,12 +148,18 @@ import java.util.concurrent.ConcurrentMap; * time_approved="1634641160229" * system_data_sync_flags="1"/> * </associations> + * + * <previously-used-ids> + * <package package_name="com.sample.companion.app"> + * <id>2</id> + * </package> + * </previously-used-ids> * </state> * }</pre> */ @SuppressLint("LongLogTag") public final class AssociationDiskStore { - private static final String TAG = "CDM_AssociationDiskStore"; + private static final String TAG = "CompanionDevice_AssociationDiskStore"; private static final int CURRENT_PERSISTENCE_VERSION = 1; @@ -156,11 +169,16 @@ public final class AssociationDiskStore { private static final String XML_TAG_STATE = "state"; private static final String XML_TAG_ASSOCIATIONS = "associations"; private static final String XML_TAG_ASSOCIATION = "association"; + private static final String XML_TAG_PREVIOUSLY_USED_IDS = "previously-used-ids"; + private static final String XML_TAG_PACKAGE = "package"; private static final String XML_TAG_TAG = "tag"; + private static final String XML_TAG_ID = "id"; private static final String XML_ATTR_PERSISTENCE_VERSION = "persistence-version"; - private static final String XML_ATTR_MAX_ID = "max-id"; private static final String XML_ATTR_ID = "id"; + // Used in <package> elements, nested within <previously-used-ids> elements. + private static final String XML_ATTR_PACKAGE_NAME = "package_name"; + // Used in <association> elements, nested within <associations> elements. private static final String XML_ATTR_PACKAGE = "package"; private static final String XML_ATTR_MAC_ADDRESS = "mac_address"; private static final String XML_ATTR_DISPLAY_NAME = "display_name"; @@ -181,12 +199,38 @@ public final class AssociationDiskStore { /** * Read all associations for given users */ - public Map<Integer, Associations> readAssociationsByUsers(@NonNull List<Integer> userIds) { - Map<Integer, Associations> userToAssociationsMap = new HashMap<>(); + public void readStateForUsers(@NonNull List<Integer> userIds, + @NonNull Set<AssociationInfo> allAssociationsOut, + @NonNull SparseArray<Map<String, Set<Integer>>> previouslyUsedIdsPerUserOut) { for (int userId : userIds) { - userToAssociationsMap.put(userId, readAssociationsByUser(userId)); + // Previously used IDs are stored in the "out" collection per-user. + final Map<String, Set<Integer>> previouslyUsedIds = new ArrayMap<>(); + + // Associations for all users are stored in a single "flat" set: so we read directly + // into it. + final Set<AssociationInfo> associationsForUser = new HashSet<>(); + readStateForUser(userId, associationsForUser, previouslyUsedIds); + + // Go through all the associations for the user and check if their IDs are within + // the allowed range (for the user). + final int firstAllowedId = getFirstAssociationIdForUser(userId); + final int lastAllowedId = getLastAssociationIdForUser(userId); + for (AssociationInfo association : associationsForUser) { + final int id = association.getId(); + if (id < firstAllowedId || id > lastAllowedId) { + Slog.e(TAG, "Wrong association ID assignment: " + id + ". " + + "Association belongs to u" + userId + " and thus its ID should be " + + "within [" + firstAllowedId + ", " + lastAllowedId + "] range."); + // TODO(b/224736262): try fixing (re-assigning) the ID? + } + } + + // Add user's association to the "output" set. + allAssociationsOut.addAll(associationsForUser); + + // Save previously used IDs for this user into the "out" structure. + previouslyUsedIdsPerUserOut.append(userId, previouslyUsedIds); } - return userToAssociationsMap; } /** @@ -196,12 +240,16 @@ public final class AssociationDiskStore { * retrieval from this datastore because it is not persisted (by design). This means that * persisted data is not guaranteed to be identical to the initial data that was stored at the * time of association. + * + * @param userId Android UserID + * @param associationsOut a container to read the {@link AssociationInfo}s "into". + * @param previouslyUsedIdsPerPackageOut a container to read the used IDs "into". */ - @NonNull - private Associations readAssociationsByUser(@UserIdInt int userId) { - Slog.i(TAG, "Reading associations for user " + userId + " from disk."); + private void readStateForUser(@UserIdInt int userId, + @NonNull Collection<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { + Slog.i(TAG, "Reading associations for user " + userId + " from disk"); final AtomicFile file = getStorageFileForUser(userId); - Associations associations; // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. @@ -212,7 +260,7 @@ public final class AssociationDiskStore { if (!file.getBaseFile().exists()) { legacyBaseFile = getBaseLegacyStorageFileForUser(userId); if (!legacyBaseFile.exists()) { - return new Associations(); + return; } readFrom = new AtomicFile(legacyBaseFile); @@ -222,12 +270,13 @@ public final class AssociationDiskStore { rootTag = XML_TAG_STATE; } - associations = readAssociationsFromFile(userId, readFrom, rootTag); + final int version = readStateFromFileLocked(userId, readFrom, rootTag, + associationsOut, previouslyUsedIdsPerPackageOut); - if (legacyBaseFile != null || associations.getVersion() < CURRENT_PERSISTENCE_VERSION) { + if (legacyBaseFile != null || version < CURRENT_PERSISTENCE_VERSION) { // The data is either in the legacy file or in the legacy format, or both. // Save the data to right file in using the current format. - writeAssociationsToFile(file, associations); + persistStateToFileLocked(file, associationsOut, previouslyUsedIdsPerPackageOut); if (legacyBaseFile != null) { // We saved the data to the right file, can delete the old file now. @@ -235,75 +284,89 @@ public final class AssociationDiskStore { } } } - return associations; } /** - * Write associations to disk for the user. + * Persisted data to the disk. + * + * Note that associatedDevice field in {@link AssociationInfo} is not persisted by this + * datastore implementation. + * + * @param userId Android UserID + * @param associations a set of user's associations. + * @param previouslyUsedIdsPerPackage a set previously used Association IDs for the user. */ - public void writeAssociationsForUser(@UserIdInt int userId, - @NonNull Associations associations) { + public void persistStateForUser(@UserIdInt int userId, + @NonNull Collection<AssociationInfo> associations, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { Slog.i(TAG, "Writing associations for user " + userId + " to disk"); final AtomicFile file = getStorageFileForUser(userId); // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize // accesses to the file on the file system using this AtomicFile object. synchronized (file) { - writeAssociationsToFile(file, associations); + persistStateToFileLocked(file, associations, previouslyUsedIdsPerPackage); } } - @NonNull - private static Associations readAssociationsFromFile(@UserIdInt int userId, - @NonNull AtomicFile file, @NonNull String rootTag) { + private int readStateFromFileLocked(@UserIdInt int userId, @NonNull AtomicFile file, + @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { try (FileInputStream in = file.openRead()) { - return readAssociationsFromInputStream(userId, in, rootTag); + return readStateFromInputStream(userId, in, rootTag, associationsOut, + previouslyUsedIdsPerPackageOut); } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Error while reading associations file", e); - return new Associations(); + return -1; } } - @NonNull - private static Associations readAssociationsFromInputStream(@UserIdInt int userId, - @NonNull InputStream in, @NonNull String rootTag) + private int readStateFromInputStream(@UserIdInt int userId, @NonNull InputStream in, + @NonNull String rootTag, @Nullable Collection<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) throws XmlPullParserException, IOException { final TypedXmlPullParser parser = Xml.resolvePullParser(in); - XmlUtils.beginDocument(parser, rootTag); + XmlUtils.beginDocument(parser, rootTag); final int version = readIntAttribute(parser, XML_ATTR_PERSISTENCE_VERSION, 0); - Associations associations = new Associations(); - switch (version) { case 0: - associations = readAssociationsV0(parser, userId); + readAssociationsV0(parser, userId, associationsOut); break; case 1: while (true) { parser.nextTag(); if (isStartOfTag(parser, XML_TAG_ASSOCIATIONS)) { - associations = readAssociationsV1(parser, userId); + readAssociationsV1(parser, userId, associationsOut); + } else if (isStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) { + readPreviouslyUsedIdsV1(parser, previouslyUsedIdsPerPackageOut); } else if (isEndOfTag(parser, rootTag)) { break; } } break; } - return associations; + return version; } - private void writeAssociationsToFile(@NonNull AtomicFile file, - @NonNull Associations associations) { + private void persistStateToFileLocked(@NonNull AtomicFile file, + @Nullable Collection<AssociationInfo> associations, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) { // Writing to file could fail, for example, if the user has been recently removed and so was // their DE (/data/system_de/<user-id>/) directory. writeToFileSafely(file, out -> { final TypedXmlSerializer serializer = Xml.resolveSerializer(out); - serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.setFeature( + "http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startDocument(null, true); serializer.startTag(null, XML_TAG_STATE); writeIntAttribute(serializer, XML_ATTR_PERSISTENCE_VERSION, CURRENT_PERSISTENCE_VERSION); + writeAssociations(serializer, associations); + writePreviouslyUsedIds(serializer, previouslyUsedIdsPerPackage); + serializer.endTag(null, XML_TAG_STATE); serializer.endDocument(); }); @@ -316,8 +379,7 @@ public final class AssociationDiskStore { * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it * possible to synchronize reads and writes to the file using the returned object. */ - @NonNull - private AtomicFile getStorageFileForUser(@UserIdInt int userId) { + private @NonNull AtomicFile getStorageFileForUser(@UserIdInt int userId) { return mUserIdToStorageFile.computeIfAbsent(userId, u -> createStorageFileForUser(userId, FILE_NAME)); } @@ -337,12 +399,14 @@ public final class AssociationDiskStore { /** * Convert payload to a set of associations */ - public static Associations readAssociationsFromPayload(byte[] payload, @UserIdInt int userId) { + public void readStateFromPayload(byte[] payload, @UserIdInt int userId, + @NonNull Set<AssociationInfo> associationsOut, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackageOut) { try (ByteArrayInputStream in = new ByteArrayInputStream(payload)) { - return readAssociationsFromInputStream(userId, in, XML_TAG_STATE); + readStateFromInputStream(userId, in, XML_TAG_STATE, associationsOut, + previouslyUsedIdsPerPackageOut); } catch (XmlPullParserException | IOException e) { Slog.e(TAG, "Error while reading associations file", e); - return new Associations(); } } @@ -350,8 +414,8 @@ public final class AssociationDiskStore { return new File(Environment.getUserSystemDirectory(userId), FILE_NAME_LEGACY); } - private static Associations readAssociationsV0(@NonNull TypedXmlPullParser parser, - @UserIdInt int userId) + private static void readAssociationsV0(@NonNull TypedXmlPullParser parser, + @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); @@ -362,70 +426,52 @@ public final class AssociationDiskStore { // means that CDM hasn't assigned any IDs yet, so we can just start from the first available // id for each user (eg. 1 for user 0; 100 001 - for user 1; 200 001 - for user 2; etc). int associationId = getFirstAssociationIdForUser(userId); - Associations associations = new Associations(); - associations.setVersion(0); - while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; - associations.addAssociation(readAssociationV0(parser, userId, associationId++)); + readAssociationV0(parser, userId, associationId++, out); } - - associations.setMaxId(associationId - 1); - - return associations; } - private static AssociationInfo readAssociationV0(@NonNull TypedXmlPullParser parser, - @UserIdInt int userId, int associationId) + private static void readAssociationV0(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, + int associationId, @NonNull Collection<AssociationInfo> out) throws XmlPullParserException { requireStartOfTag(parser, XML_TAG_ASSOCIATION); final String appPackage = readStringAttribute(parser, XML_ATTR_PACKAGE); final String tag = readStringAttribute(parser, XML_TAG_TAG); final String deviceAddress = readStringAttribute(parser, LEGACY_XML_ATTR_DEVICE); + + if (appPackage == null || deviceAddress == null) return; + final String profile = readStringAttribute(parser, XML_ATTR_PROFILE); final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY); final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L); - return new AssociationInfo(associationId, userId, appPackage, tag, + out.add(new AssociationInfo(associationId, userId, appPackage, tag, MacAddress.fromString(deviceAddress), null, profile, null, /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false, - timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); + timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0)); } - private static Associations readAssociationsV1(@NonNull TypedXmlPullParser parser, - @UserIdInt int userId) + private static void readAssociationsV1(@NonNull TypedXmlPullParser parser, + @UserIdInt int userId, @NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATIONS); - // For old builds that don't have max-id attr, - // default maxId to 0 and get the maxId out of all association ids. - int maxId = readIntAttribute(parser, XML_ATTR_MAX_ID, 0); - Associations associations = new Associations(); - associations.setVersion(1); - while (true) { parser.nextTag(); if (isEndOfTag(parser, XML_TAG_ASSOCIATIONS)) break; if (!isStartOfTag(parser, XML_TAG_ASSOCIATION)) continue; - AssociationInfo association = readAssociationV1(parser, userId); - associations.addAssociation(association); - - maxId = Math.max(maxId, association.getId()); + readAssociationV1(parser, userId, out); } - - associations.setMaxId(maxId); - - return associations; } - private static AssociationInfo readAssociationV1(@NonNull TypedXmlPullParser parser, - @UserIdInt int userId) - throws XmlPullParserException, IOException { + private static void readAssociationV1(@NonNull TypedXmlPullParser parser, @UserIdInt int userId, + @NonNull Collection<AssociationInfo> out) throws XmlPullParserException, IOException { requireStartOfTag(parser, XML_TAG_ASSOCIATION); final int associationId = readIntAttribute(parser, XML_ATTR_ID); @@ -445,19 +491,46 @@ public final class AssociationDiskStore { final int systemDataSyncFlags = readIntAttribute(parser, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, 0); - return new AssociationInfo(associationId, userId, appPackage, tag, macAddress, displayName, - profile, null, selfManaged, notify, revoked, pending, timeApproved, - lastTimeConnected, systemDataSyncFlags); + final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId, + appPackage, tag, macAddress, displayName, profile, selfManaged, notify, revoked, + pending, timeApproved, lastTimeConnected, systemDataSyncFlags); + if (associationInfo != null) { + out.add(associationInfo); + } + } + + private static void readPreviouslyUsedIdsV1(@NonNull TypedXmlPullParser parser, + @NonNull Map<String, Set<Integer>> out) throws XmlPullParserException, IOException { + requireStartOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS); + + while (true) { + parser.nextTag(); + if (isEndOfTag(parser, XML_TAG_PREVIOUSLY_USED_IDS)) break; + if (!isStartOfTag(parser, XML_TAG_PACKAGE)) continue; + + final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE_NAME); + final Set<Integer> usedIds = new HashSet<>(); + + while (true) { + parser.nextTag(); + if (isEndOfTag(parser, XML_TAG_PACKAGE)) break; + if (!isStartOfTag(parser, XML_TAG_ID)) continue; + + parser.nextToken(); + final int id = Integer.parseInt(parser.getText()); + usedIds.add(id); + } + + out.put(packageName, usedIds); + } } private static void writeAssociations(@NonNull XmlSerializer parent, - @NonNull Associations associations) - throws IOException { + @Nullable Collection<AssociationInfo> associations) throws IOException { final XmlSerializer serializer = parent.startTag(null, XML_TAG_ASSOCIATIONS); - for (AssociationInfo association : associations.getAssociations()) { + for (AssociationInfo association : associations) { writeAssociation(serializer, association); } - writeIntAttribute(serializer, XML_ATTR_MAX_ID, associations.getMaxId()); serializer.endTag(null, XML_TAG_ASSOCIATIONS); } @@ -484,6 +557,26 @@ public final class AssociationDiskStore { serializer.endTag(null, XML_TAG_ASSOCIATION); } + private static void writePreviouslyUsedIds(@NonNull XmlSerializer parent, + @NonNull Map<String, Set<Integer>> previouslyUsedIdsPerPackage) throws IOException { + final XmlSerializer serializer = parent.startTag(null, XML_TAG_PREVIOUSLY_USED_IDS); + for (Map.Entry<String, Set<Integer>> entry : previouslyUsedIdsPerPackage.entrySet()) { + writePreviouslyUsedIdsForPackage(serializer, entry.getKey(), entry.getValue()); + } + serializer.endTag(null, XML_TAG_PREVIOUSLY_USED_IDS); + } + + private static void writePreviouslyUsedIdsForPackage(@NonNull XmlSerializer parent, + @NonNull String packageName, @NonNull Set<Integer> previouslyUsedIds) + throws IOException { + final XmlSerializer serializer = parent.startTag(null, XML_TAG_PACKAGE); + writeStringAttribute(serializer, XML_ATTR_PACKAGE_NAME, packageName); + forEach(previouslyUsedIds, id -> serializer.startTag(null, XML_TAG_ID) + .text(Integer.toString(id)) + .endTag(null, XML_TAG_ID)); + serializer.endTag(null, XML_TAG_PACKAGE); + } + private static void requireStartOfTag(@NonNull XmlPullParser parser, @NonNull String tag) throws XmlPullParserException { if (isStartOfTag(parser, tag)) return; @@ -494,4 +587,22 @@ public final class AssociationDiskStore { private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { return address != null ? MacAddress.fromString(address) : null; } + + private static AssociationInfo createAssociationInfoNoThrow(int associationId, + @UserIdInt int userId, @NonNull String appPackage, @Nullable String tag, + @Nullable MacAddress macAddress, @Nullable CharSequence displayName, + @Nullable String profile, boolean selfManaged, boolean notify, boolean revoked, + boolean pending, long timeApproved, long lastTimeConnected, int systemDataSyncFlags) { + AssociationInfo associationInfo = null; + try { + // We do not persist AssociatedDevice, which means that AssociationInfo retrieved from + // datastore is not guaranteed to be identical to the one from initial association. + associationInfo = new AssociationInfo(associationId, userId, appPackage, tag, + macAddress, displayName, profile, null, selfManaged, notify, + revoked, pending, timeApproved, lastTimeConnected, systemDataSyncFlags); + } catch (Exception e) { + Slog.e(TAG, "Could not create AssociationInfo", e); + } + return associationInfo; + } } diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index a02d9f912bcd..29ec7c2c9743 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -24,6 +24,7 @@ import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR; import static android.content.ComponentName.createRelative; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; import static com.android.server.companion.utils.PermissionsUtils.enforcePermissionForCreatingAssociation; import static com.android.server.companion.utils.RolesUtils.addRoleHolderForAssociation; @@ -127,16 +128,17 @@ public class AssociationRequestsProcessor { private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min; private final @NonNull Context mContext; - private final @NonNull PackageManagerInternal mPackageManagerInternal; + private final @NonNull CompanionDeviceManagerService mService; + private final @NonNull PackageManagerInternal mPackageManager; private final @NonNull AssociationStore mAssociationStore; @NonNull private final ComponentName mCompanionDeviceActivity; - public AssociationRequestsProcessor(@NonNull Context context, - @NonNull PackageManagerInternal packageManagerInternal, + public AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service, @NonNull AssociationStore associationStore) { - mContext = context; - mPackageManagerInternal = packageManagerInternal; + mContext = service.getContext(); + mService = service; + mPackageManager = service.mPackageManagerInternal; mAssociationStore = associationStore; mCompanionDeviceActivity = createRelative( mContext.getString(R.string.config_companionDeviceManagerPackage), @@ -158,7 +160,7 @@ public class AssociationRequestsProcessor { requireNonNull(packageName, "Package name MUST NOT be null"); requireNonNull(callback, "Callback MUST NOT be null"); - final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); + final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); Slog.d(TAG, "processNewAssociationRequest() " + "request=" + request + ", " + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")"); @@ -224,7 +226,7 @@ public class AssociationRequestsProcessor { enforceUsesCompanionDeviceFeature(mContext, userId, packageName); - final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); + final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); final Bundle extras = new Bundle(); extras.putBoolean(EXTRA_FORCE_CANCEL_CONFIRMATION, true); @@ -241,7 +243,7 @@ public class AssociationRequestsProcessor { @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) { final String packageName = request.getPackageName(); final int userId = request.getUserId(); - final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); + final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId); // 1. Need to check permissions again in case something changed, since we first received // this request. @@ -265,12 +267,15 @@ public class AssociationRequestsProcessor { @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver) { - Binder.withCleanCallingIdentity(() -> { + final long callingIdentity = Binder.clearCallingIdentity(); + try { createAssociation(userId, packageName, macAddress, request.getDisplayName(), request.getDeviceProfile(), request.getAssociatedDevice(), request.isSelfManaged(), callback, resultReceiver); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentity); + } } /** @@ -281,7 +286,7 @@ public class AssociationRequestsProcessor { @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { - final int id = mAssociationStore.getNextId(userId); + final int id = mService.getNewAssociationIdForPackage(userId, packageName); final long timestamp = System.currentTimeMillis(); final AssociationInfo association = new AssociationInfo(id, userId, packageName, @@ -291,6 +296,10 @@ public class AssociationRequestsProcessor { // Add role holder for association (if specified) and add new association to store. maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); + + // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since + // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case + // that there are other devices with the same profile, so the role holder won't be removed. } /** @@ -302,12 +311,12 @@ public class AssociationRequestsProcessor { // If the "Device Profile" is specified, make the companion application a holder of the // corresponding role. // If it is null, then the operation will succeed without granting any role. - addRoleHolderForAssociation(mContext, association, success -> { + addRoleHolderForAssociation(mService.getContext(), association, success -> { if (success) { Slog.i(TAG, "Added " + association.getDeviceProfile() + " role to userId=" + association.getUserId() + ", packageName=" + association.getPackageName()); - mAssociationStore.addAssociation(association); + addAssociationToStore(association); sendCallbackAndFinish(association, callback, resultReceiver); } else { Slog.e(TAG, "Failed to add u" + association.getUserId() @@ -338,6 +347,17 @@ public class AssociationRequestsProcessor { mAssociationStore.updateAssociation(updated); } + private void addAssociationToStore(@NonNull AssociationInfo association) { + Slog.i(TAG, "New CDM association created=" + association); + + mAssociationStore.addAssociation(association); + + mService.updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); + + logCreateAssociation(association.getDeviceProfile()); + } + private void sendCallbackAndFinish(@Nullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { @@ -389,22 +409,27 @@ public class AssociationRequestsProcessor { private PendingIntent createPendingIntent(int packageUid, Intent intent) { final PendingIntent pendingIntent; + final long token = Binder.clearCallingIdentity(); // Using uid of the application that will own the association (usually the same // application that sent the request) allows us to have multiple "pending" association // requests at the same time. // If the application already has a pending association request, that PendingIntent // will be cancelled except application wants to cancel the request by the system. - return Binder.withCleanCallingIdentity(() -> - PendingIntent.getActivityAsUser( + try { + pendingIntent = PendingIntent.getActivityAsUser( mContext, /*requestCode */ packageUid, intent, FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, ActivityOptions.makeBasic() .setPendingIntentCreatorBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) .toBundle(), - UserHandle.CURRENT) - ); + UserHandle.CURRENT); + } finally { + Binder.restoreCallingIdentity(token); + } + + return pendingIntent; } private final ResultReceiver mOnRequestConfirmationReceiver = @@ -445,7 +470,7 @@ public class AssociationRequestsProcessor { // Throttle frequent associations final long now = System.currentTimeMillis(); final List<AssociationInfo> associationForPackage = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + mAssociationStore.getAssociationsForPackage(userId, packageName); // Number of "recent" associations. int recent = 0; for (AssociationInfo association : associationForPackage) { @@ -461,6 +486,6 @@ public class AssociationRequestsProcessor { } } - return PackageUtils.isPackageAllowlisted(mContext, mPackageManagerInternal, packageName); + return PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName); } } diff --git a/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java new file mode 100644 index 000000000000..d1efbbcd3411 --- /dev/null +++ b/services/companion/java/com/android/server/companion/association/AssociationRevokeProcessor.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.association; + +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; + +import static com.android.internal.util.CollectionUtils.any; +import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation; +import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation; +import static com.android.server.companion.CompanionDeviceManagerService.PerUserAssociationSet; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.app.ActivityManager; +import android.companion.AssociationInfo; +import android.content.Context; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.companion.CompanionApplicationController; +import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; +import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.transport.CompanionTransportManager; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A class response for Association removal. + */ +@SuppressLint("LongLogTag") +public class AssociationRevokeProcessor { + + private static final String TAG = "CDM_AssociationRevokeProcessor"; + private static final boolean DEBUG = false; + private final @NonNull Context mContext; + private final @NonNull CompanionDeviceManagerService mService; + private final @NonNull AssociationStore mAssociationStore; + private final @NonNull PackageManagerInternal mPackageManagerInternal; + private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final @NonNull SystemDataTransferRequestStore mSystemDataTransferRequestStore; + private final @NonNull CompanionApplicationController mCompanionAppController; + private final @NonNull CompanionTransportManager mTransportManager; + private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; + private final ActivityManager mActivityManager; + + /** + * A structure that consists of a set of revoked associations that pending for role holder + * removal per each user. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval = + new PerUserAssociationSet(); + /** + * Contains uid-s of packages pending to be removed from the role holder list (after + * revocation of an association), which will happen one the package is no longer visible to the + * user. + * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but + * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived + * from uid-s using {@link UserHandle#getUserId(int)}). + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #addToPendingRoleHolderRemoval(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + */ + @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval") + private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>(); + + public AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service, + @NonNull AssociationStore associationStore, + @NonNull PackageManagerInternal packageManager, + @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, + @NonNull CompanionApplicationController applicationController, + @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, + @NonNull CompanionTransportManager companionTransportManager) { + mService = service; + mContext = service.getContext(); + mActivityManager = mContext.getSystemService(ActivityManager.class); + mAssociationStore = associationStore; + mPackageManagerInternal = packageManager; + mOnPackageVisibilityChangeListener = + new OnPackageVisibilityChangeListener(mActivityManager); + mDevicePresenceMonitor = devicePresenceMonitor; + mCompanionAppController = applicationController; + mSystemDataTransferRequestStore = systemDataTransferRequestStore; + mTransportManager = companionTransportManager; + } + + /** + * Disassociate an association + */ + // TODO: also revoke notification access + public void disassociateInternal(int associationId) { + final AssociationInfo association = mAssociationStore.getAssociationById(associationId); + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final String deviceProfile = association.getDeviceProfile(); + + // Detach transport if exists + mTransportManager.detachSystemDataTransport(packageName, userId, associationId); + + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Need to remove the app from list of the role holders, but will have to do it later + // (the app is in foreground at the moment). + addToPendingRoleHolderRemoval(association); + } + + // Need to check if device still present now because CompanionDevicePresenceMonitor will + // remove current connected device after mAssociationStore.removeAssociation + final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId); + + // Removing the association. + mAssociationStore.removeAssociation(associationId); + // Do not need to persistUserState since CompanionDeviceManagerService will get callback + // from #onAssociationChanged, and it will handle the persistUserState which including + // active and revoked association. + logRemoveAssociation(deviceProfile); + + // Remove all the system data transfer requests for the association. + mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, associationId); + + if (!wasPresent || !association.isNotifyOnDeviceNearby()) return; + // The device was connected and the app was notified: check if we need to unbind the app + // now. + final boolean shouldStayBound = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), + it -> it.isNotifyOnDeviceNearby() + && mDevicePresenceMonitor.isDevicePresent(it.getId())); + if (shouldStayBound) return; + mCompanionAppController.unbindCompanionApplication(userId, packageName); + } + + /** + * First, checks if the companion application should be removed from the list role holders when + * upon association's removal, i.e.: association's profile (matches the role) is not null, + * the application does not have other associations with the same profile, etc. + * + * <p> + * Then, if establishes that the application indeed has to be removed from the list of the role + * holders, checks if it could be done right now - + * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()} + * will kill the application's process, which leads poor user experience if the application was + * in foreground when this happened, to avoid this CDMS delays invoking + * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground. + * + * @return {@code true} if the application does NOT need be removed from the list of the role + * holders OR if the application was successfully removed from the list of role holders. + * I.e.: from the role-management perspective the association is done with. + * {@code false} if the application needs to be removed from the list of role the role + * holders, BUT it CDMS would prefer to do it later. + * I.e.: application is in the foreground at the moment, but invoking + * {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process, + * which would lead to the poor UX, hence need to try later. + */ + public boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) { + if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association); + final String deviceProfile = association.getDeviceProfile(); + + if (deviceProfile == null) { + // No role was granted to for this association, there is nothing else we need to here. + return true; + } + // Do not need to remove the system role since it was pre-granted by the system. + if (deviceProfile.equals(DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) { + return true; + } + + // Check if the applications is associated with another devices with the profile. If so, + // it should remain the role holder. + final int id = association.getId(); + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final boolean roleStillInUse = any( + mAssociationStore.getAssociationsForPackage(userId, packageName), + it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId()); + if (roleStillInUse) { + // Application should remain a role holder, there is nothing else we need to here. + return true; + } + + final int packageProcessImportance = getPackageProcessImportance(userId, packageName); + if (packageProcessImportance <= IMPORTANCE_VISIBLE) { + // Need to remove the app from the list of role holders, but the process is visible to + // the user at the moment, so we'll need to it later: log and return false. + Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id + + " now - process is visible."); + return false; + } + + removeRoleHolderForAssociation(mContext, association.getUserId(), + association.getPackageName(), association.getDeviceProfile()); + return true; + } + + /** + * Set revoked flag for active association and add the revoked association and the uid into + * the caches. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + public void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + // First: set revoked flag + association = (new AssociationInfo.Builder(association)).setRevoked(true).build(); + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + // Second: add to the set. + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId()) + .add(association); + if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) { + mUidsPendingRoleHolderRemoval.put(uid, packageName); + + if (mUidsPendingRoleHolderRemoval.size() == 1) { + // Just added first uid: start the listener + mOnPackageVisibilityChangeListener.startListening(); + } + } + } + } + + /** + * @return a copy of the revoked associations set (safeguarding against + * {@code ConcurrentModificationException}-s). + */ + @NonNull + public Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser( + @UserIdInt int userId) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + // Return a copy. + return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)); + } + } + + @SuppressLint("MissingPermission") + private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { + return Binder.withCleanCallingIdentity(() -> { + final int uid = + mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); + return mActivityManager.getUidImportance(uid); + }); + } + + /** + * Remove the revoked association from the cache and also remove the uid from the map if + * there are other associations with the same package still pending for role holder removal. + * + * @see #mRevokedAssociationsPendingRoleHolderRemoval + * @see #mUidsPendingRoleHolderRemoval + * @see OnPackageVisibilityChangeListener + */ + private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) { + final String packageName = association.getPackageName(); + final int userId = association.getUserId(); + final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */ 0, userId); + + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId) + .remove(association); + + final boolean shouldKeepUidForRemoval = any( + getPendingRoleHolderRemovalAssociationsForUser(userId), + ai -> packageName.equals(ai.getPackageName())); + // Do not remove the uid from the map since other associations with + // the same packageName still pending for role holder removal. + if (!shouldKeepUidForRemoval) { + mUidsPendingRoleHolderRemoval.remove(uid); + } + + if (mUidsPendingRoleHolderRemoval.isEmpty()) { + // The set is empty now - can "turn off" the listener. + mOnPackageVisibilityChangeListener.stopListening(); + } + } + } + + private String getPackageNameByUid(int uid) { + synchronized (mRevokedAssociationsPendingRoleHolderRemoval) { + return mUidsPendingRoleHolderRemoval.get(uid); + } + } + + /** + * An OnUidImportanceListener class which watches the importance of the packages. + * In this class, we ONLY interested in the importance of the running process is greater than + * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE} for the uids have been added + * into the {@link #mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the + * revoked associations for the same packages. + * + * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo) + * @see #removeFromPendingRoleHolderRemoval(AssociationInfo) + * @see #getPendingRoleHolderRemovalAssociationsForUser(int) + */ + private class OnPackageVisibilityChangeListener implements + ActivityManager.OnUidImportanceListener { + final @NonNull ActivityManager mAm; + + OnPackageVisibilityChangeListener(@NonNull ActivityManager am) { + this.mAm = am; + } + + @SuppressLint("MissingPermission") + void startListening() { + Binder.withCleanCallingIdentity( + () -> mAm.addOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this, + ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); + } + + @SuppressLint("MissingPermission") + void stopListening() { + Binder.withCleanCallingIdentity( + () -> mAm.removeOnUidImportanceListener( + /* listener */ OnPackageVisibilityChangeListener.this)); + } + + @Override + public void onUidImportance(int uid, int importance) { + if (importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + // The lower the importance value the more "important" the process is. + // We are only interested when the process ceases to be visible. + return; + } + + final String packageName = getPackageNameByUid(uid); + if (packageName == null) { + // Not interested in this uid. + return; + } + + final int userId = UserHandle.getUserId(uid); + + boolean needToPersistStateForUser = false; + + for (AssociationInfo association : + getPendingRoleHolderRemovalAssociationsForUser(userId)) { + if (!packageName.equals(association.getPackageName())) continue; + + if (!maybeRemoveRoleHolderForAssociation(association)) { + // Did not remove the role holder, will have to try again later. + continue; + } + + removeFromPendingRoleHolderRemoval(association); + needToPersistStateForUser = true; + } + + if (needToPersistStateForUser) { + mService.postPersistUserState(userId); + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index 29de764c4d5f..2f94bdebb988 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -16,24 +16,15 @@ package com.android.server.companion.association; -import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; -import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation; - import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; -import android.companion.IOnAssociationsChangedListener; -import android.content.pm.UserInfo; import android.net.MacAddress; -import android.os.Binder; -import android.os.RemoteCallbackList; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; import android.util.Slog; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; @@ -42,14 +33,15 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; /** * Association store for CRUD. @@ -117,105 +109,44 @@ public class AssociationStore { private final Object mLock = new Object(); - private final ExecutorService mExecutor; - @GuardedBy("mLock") - private boolean mPersisted = false; + private final Map<Integer, AssociationInfo> mIdMap = new HashMap<>(); @GuardedBy("mLock") - private final Map<Integer, AssociationInfo> mIdToAssociationMap = new HashMap<>(); + private final Map<MacAddress, Set<Integer>> mAddressMap = new HashMap<>(); @GuardedBy("mLock") - private final Map<Integer, Integer> mUserToMaxId = new HashMap<>(); - - @GuardedBy("mLocalListeners") - private final Set<OnChangeListener> mLocalListeners = new LinkedHashSet<>(); - @GuardedBy("mRemoteListeners") - private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners = - new RemoteCallbackList<>(); - - private final UserManager mUserManager; - private final AssociationDiskStore mDiskStore; - - public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) { - mUserManager = userManager; - mDiskStore = diskStore; - mExecutor = Executors.newSingleThreadExecutor(); - } - - /** - * Load all alive users' associations from disk to cache. - */ - public void refreshCache() { - Binder.withCleanCallingIdentity(() -> { - List<Integer> userIds = new ArrayList<>(); - for (UserInfo user : mUserManager.getAliveUsers()) { - userIds.add(user.id); - } - - synchronized (mLock) { - mPersisted = false; - - mIdToAssociationMap.clear(); - mUserToMaxId.clear(); - - // The data is stored in DE directories, so we can read the data for all users now - // (which would not be possible if the data was stored to CE directories). - Map<Integer, Associations> userToAssociationsMap = - mDiskStore.readAssociationsByUsers(userIds); - for (Map.Entry<Integer, Associations> entry : userToAssociationsMap.entrySet()) { - for (AssociationInfo association : entry.getValue().getAssociations()) { - mIdToAssociationMap.put(association.getId(), association); - } - mUserToMaxId.put(entry.getKey(), entry.getValue().getMaxId()); - } - - mPersisted = true; - } - }); - } - - /** - * Get the current max association id. - */ - public int getMaxId(int userId) { - synchronized (mLock) { - return mUserToMaxId.getOrDefault(userId, 0); - } - } + private final SparseArray<List<AssociationInfo>> mCachedPerUser = new SparseArray<>(); - /** - * Get the next available association id. - */ - public int getNextId(int userId) { - synchronized (mLock) { - return getMaxId(userId) + 1; - } - } + @GuardedBy("mListeners") + private final Set<OnChangeListener> mListeners = new LinkedHashSet<>(); /** * Add an association. */ public void addAssociation(@NonNull AssociationInfo association) { - Slog.i(TAG, "Adding new association=[" + association + "]..."); + Slog.i(TAG, "Adding new association=" + association); + + // Validity check first. + checkNotRevoked(association); final int id = association.getId(); - final int userId = association.getUserId(); synchronized (mLock) { - if (mIdToAssociationMap.containsKey(id)) { - Slog.e(TAG, "Association with id=[" + id + "] already exists."); + if (mIdMap.containsKey(id)) { + Slog.e(TAG, "Association with id " + id + " already exists."); return; } + mIdMap.put(id, association); - mIdToAssociationMap.put(id, association); - mUserToMaxId.put(userId, Math.max(mUserToMaxId.getOrDefault(userId, 0), id)); + final MacAddress address = association.getDeviceMacAddress(); + if (address != null) { + mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id); + } - writeCacheToDisk(userId); + invalidateCacheForUserLocked(association.getUserId()); Slog.i(TAG, "Done adding new association."); } - logCreateAssociation(association.getDeviceProfile()); - broadcastChange(CHANGE_TYPE_ADDED, association); } @@ -223,16 +154,18 @@ public class AssociationStore { * Update an association. */ public void updateAssociation(@NonNull AssociationInfo updated) { - Slog.i(TAG, "Updating new association=[" + updated + "]..."); + Slog.i(TAG, "Updating new association=" + updated); + // Validity check first. + checkNotRevoked(updated); final int id = updated.getId(); + final AssociationInfo current; final boolean macAddressChanged; - synchronized (mLock) { - current = mIdToAssociationMap.get(id); + current = mIdMap.get(id); if (current == null) { - Slog.w(TAG, "Can't update association id=[" + id + "]. It does not exist."); + Slog.w(TAG, "Can't update association. It does not exist."); return; } @@ -241,238 +174,174 @@ public class AssociationStore { return; } - mIdToAssociationMap.put(id, updated); - - writeCacheToDisk(updated.getUserId()); + // Update the ID-to-Association map. + mIdMap.put(id, updated); + // Invalidate the corresponding user cache entry. + invalidateCacheForUserLocked(current.getUserId()); + + // Update the MacAddress-to-List<Association> map if needed. + final MacAddress updatedAddress = updated.getDeviceMacAddress(); + final MacAddress currentAddress = current.getDeviceMacAddress(); + macAddressChanged = !Objects.equals(currentAddress, updatedAddress); + if (macAddressChanged) { + if (currentAddress != null) { + mAddressMap.get(currentAddress).remove(id); + } + if (updatedAddress != null) { + mAddressMap.computeIfAbsent(updatedAddress, it -> new HashSet<>()).add(id); + } + } + Slog.i(TAG, "Done updating association."); } - Slog.i(TAG, "Done updating association."); - - // Check if the MacAddress has changed. - final MacAddress updatedAddress = updated.getDeviceMacAddress(); - final MacAddress currentAddress = current.getDeviceMacAddress(); - macAddressChanged = !Objects.equals(currentAddress, updatedAddress); - final int changeType = macAddressChanged ? CHANGE_TYPE_UPDATED_ADDRESS_CHANGED : CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED; - broadcastChange(changeType, updated); } /** - * Remove an association. + * Remove an association */ public void removeAssociation(int id) { - Slog.i(TAG, "Removing association id=[" + id + "]..."); + Slog.i(TAG, "Removing association id=" + id); final AssociationInfo association; - synchronized (mLock) { - association = mIdToAssociationMap.remove(id); + association = mIdMap.remove(id); if (association == null) { - Slog.w(TAG, "Can't remove association id=[" + id + "]. It does not exist."); + Slog.w(TAG, "Can't remove association. It does not exist."); return; } - writeCacheToDisk(association.getUserId()); - - Slog.i(TAG, "Done removing association."); - } - - logRemoveAssociation(association.getDeviceProfile()); - - broadcastChange(CHANGE_TYPE_REMOVED, association); - } - - private void writeCacheToDisk(@UserIdInt int userId) { - mExecutor.execute(() -> { - Associations associations = new Associations(); - synchronized (mLock) { - associations.setMaxId(mUserToMaxId.getOrDefault(userId, 0)); - associations.setAssociations( - CollectionUtils.filter(mIdToAssociationMap.values().stream().toList(), - a -> a.getUserId() == userId)); + final MacAddress macAddress = association.getDeviceMacAddress(); + if (macAddress != null) { + mAddressMap.get(macAddress).remove(id); } - mDiskStore.writeAssociationsForUser(userId, associations); - }); - } - /** - * Get a copy of all associations including pending and revoked ones. - * Modifying the copy won't modify the actual associations. - * - * If a cache miss happens, read from disk. - */ - @NonNull - public List<AssociationInfo> getAssociations() { - synchronized (mLock) { - if (!mPersisted) { - refreshCache(); - } - return List.copyOf(mIdToAssociationMap.values()); - } - } + invalidateCacheForUserLocked(association.getUserId()); - /** - * Get a copy of active associations. - */ - @NonNull - public List<AssociationInfo> getActiveAssociations() { - synchronized (mLock) { - return CollectionUtils.filter(getAssociations(), AssociationInfo::isActive); + Slog.i(TAG, "Done removing association."); } - } - /** - * Get a copy of all associations by user. - */ - @NonNull - public List<AssociationInfo> getAssociationsByUser(@UserIdInt int userId) { - synchronized (mLock) { - return CollectionUtils.filter(getAssociations(), a -> a.getUserId() == userId); - } + broadcastChange(CHANGE_TYPE_REMOVED, association); } /** - * Get a copy of active associations by user. + * @return a "snapshot" of the current state of the existing associations. */ - @NonNull - public List<AssociationInfo> getActiveAssociationsByUser(@UserIdInt int userId) { + public @NonNull Collection<AssociationInfo> getAssociations() { synchronized (mLock) { - return CollectionUtils.filter(getActiveAssociations(), a -> a.getUserId() == userId); + // IMPORTANT: make and return a COPY of the mIdMap.values(), NOT a "direct" reference. + // The HashMap.values() returns a collection which is backed by the HashMap, so changes + // to the HashMap are reflected in this collection. + // For us this means that if mIdMap is modified while the iteration over mIdMap.values() + // is in progress it may lead to "undefined results" (according to the HashMap's + // documentation) or cause ConcurrentModificationExceptions in the iterator (according + // to the bugreports...). + return List.copyOf(mIdMap.values()); } } /** - * Get a copy of all associations by package. + * Get associations for the user. */ - @NonNull - public List<AssociationInfo> getAssociationsByPackage(@UserIdInt int userId, - @NonNull String packageName) { + public @NonNull List<AssociationInfo> getAssociationsForUser(@UserIdInt int userId) { synchronized (mLock) { - return CollectionUtils.filter(getAssociationsByUser(userId), - a -> a.getPackageName().equals(packageName)); + return getAssociationsForUserLocked(userId); } } /** - * Get a copy of active associations by package. + * Get associations for the package */ - @NonNull - public List<AssociationInfo> getActiveAssociationsByPackage(@UserIdInt int userId, - @NonNull String packageName) { - synchronized (mLock) { - return CollectionUtils.filter(getActiveAssociationsByUser(userId), - a -> a.getPackageName().equals(packageName)); - } + public @NonNull List<AssociationInfo> getAssociationsForPackage( + @UserIdInt int userId, @NonNull String packageName) { + final List<AssociationInfo> associationsForUser = getAssociationsForUser(userId); + final List<AssociationInfo> associationsForPackage = + CollectionUtils.filter(associationsForUser, + it -> it.getPackageName().equals(packageName)); + return Collections.unmodifiableList(associationsForPackage); } /** - * Get the first active association with the mac address. + * Get associations by mac address for the package. */ - @Nullable - public AssociationInfo getFirstAssociationByAddress( + public @Nullable AssociationInfo getAssociationsForPackageWithAddress( @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { - synchronized (mLock) { - return CollectionUtils.find(getActiveAssociationsByPackage(userId, packageName), - a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress() - .equals(MacAddress.fromString(macAddress))); - } + final List<AssociationInfo> associations = getAssociationsByAddress(macAddress); + return CollectionUtils.find(associations, + it -> it.belongsToPackage(userId, packageName)); } /** - * Get the association by id. + * Get association by id. */ - @Nullable - public AssociationInfo getAssociationById(int id) { + public @Nullable AssociationInfo getAssociationById(int id) { synchronized (mLock) { - return mIdToAssociationMap.get(id); + return mIdMap.get(id); } } /** - * Get a copy of active associations by mac address. + * Get associations by mac address. */ @NonNull - public List<AssociationInfo> getActiveAssociationsByAddress(@NonNull String macAddress) { - synchronized (mLock) { - return CollectionUtils.filter(getActiveAssociations(), - a -> a.getDeviceMacAddress() != null && a.getDeviceMacAddress() - .equals(MacAddress.fromString(macAddress))); - } - } + public List<AssociationInfo> getAssociationsByAddress(@NonNull String macAddress) { + final MacAddress address = MacAddress.fromString(macAddress); - /** - * Get a copy of revoked associations. - */ - @NonNull - public List<AssociationInfo> getRevokedAssociations() { synchronized (mLock) { - return CollectionUtils.filter(getAssociations(), AssociationInfo::isRevoked); - } - } + final Set<Integer> ids = mAddressMap.get(address); + if (ids == null) return Collections.emptyList(); - /** - * Get a copy of revoked associations for the package. - */ - @NonNull - public List<AssociationInfo> getRevokedAssociations(@UserIdInt int userId, - @NonNull String packageName) { - synchronized (mLock) { - return CollectionUtils.filter(getAssociations(), - a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId - && a.isRevoked()); + final List<AssociationInfo> associations = new ArrayList<>(ids.size()); + for (Integer id : ids) { + associations.add(mIdMap.get(id)); + } + + return Collections.unmodifiableList(associations); } } - /** - * Get a copy of active associations. - */ + @GuardedBy("mLock") @NonNull - public List<AssociationInfo> getPendingAssociations(@UserIdInt int userId, - @NonNull String packageName) { - synchronized (mLock) { - return CollectionUtils.filter(getAssociations(), - a -> packageName.equals(a.getPackageName()) && a.getUserId() == userId - && a.isPending()); + private List<AssociationInfo> getAssociationsForUserLocked(@UserIdInt int userId) { + final List<AssociationInfo> cached = mCachedPerUser.get(userId); + if (cached != null) { + return cached; } - } - /** - * Register a local listener for association changes. - */ - public void registerLocalListener(@NonNull OnChangeListener listener) { - synchronized (mLocalListeners) { - mLocalListeners.add(listener); + final List<AssociationInfo> associationsForUser = new ArrayList<>(); + for (AssociationInfo association : mIdMap.values()) { + if (association.getUserId() == userId) { + associationsForUser.add(association); + } } + final List<AssociationInfo> set = Collections.unmodifiableList(associationsForUser); + mCachedPerUser.set(userId, set); + return set; } - /** - * Unregister a local listener previously registered for association changes. - */ - public void unregisterLocalListener(@NonNull OnChangeListener listener) { - synchronized (mLocalListeners) { - mLocalListeners.remove(listener); - } + @GuardedBy("mLock") + private void invalidateCacheForUserLocked(@UserIdInt int userId) { + mCachedPerUser.delete(userId); } /** - * Register a remote listener for association changes. + * Register a listener for association changes. */ - public void registerRemoteListener(@NonNull IOnAssociationsChangedListener listener, - int userId) { - synchronized (mRemoteListeners) { - mRemoteListeners.register(listener, userId); + public void registerListener(@NonNull OnChangeListener listener) { + synchronized (mListeners) { + mListeners.add(listener); } } /** - * Unregister a remote listener previously registered for association changes. + * Unregister a listener previously registered for association changes. */ - public void unregisterRemoteListener(@NonNull IOnAssociationsChangedListener listener) { - synchronized (mRemoteListeners) { - mRemoteListeners.unregister(listener); + public void unregisterListener(@NonNull OnChangeListener listener) { + synchronized (mListeners) { + mListeners.remove(listener); } } @@ -481,39 +350,52 @@ public class AssociationStore { */ public void dump(@NonNull PrintWriter out) { out.append("Companion Device Associations: "); - if (getActiveAssociations().isEmpty()) { + if (getAssociations().isEmpty()) { out.append("<empty>\n"); } else { out.append("\n"); - for (AssociationInfo a : getActiveAssociations()) { + for (AssociationInfo a : getAssociations()) { out.append(" ").append(a.toString()).append('\n'); } } } private void broadcastChange(@ChangeType int changeType, AssociationInfo association) { - synchronized (mLocalListeners) { - for (OnChangeListener listener : mLocalListeners) { + synchronized (mListeners) { + for (OnChangeListener listener : mListeners) { listener.onAssociationChanged(changeType, association); } } - synchronized (mRemoteListeners) { - final int userId = association.getUserId(); - final List<AssociationInfo> updatedAssociations = getActiveAssociationsByUser(userId); - // Notify listeners if ADDED, REMOVED or UPDATED_ADDRESS_CHANGED. - // Do NOT notify when UPDATED_ADDRESS_UNCHANGED, which means a minor tweak in - // association's configs, which "listeners" won't (and shouldn't) be able to see. - if (changeType != CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED) { - mRemoteListeners.broadcast((listener, callbackUserId) -> { - int listenerUserId = (int) callbackUserId; - if (listenerUserId == userId || listenerUserId == UserHandle.USER_ALL) { - try { - listener.onAssociationsChanged(updatedAssociations); - } catch (RemoteException ignored) { - } - } - }); + } + + /** + * Set associations to cache. It will clear the existing cache. + */ + public void setAssociationsToCache(Collection<AssociationInfo> associations) { + // Validity check first. + associations.forEach(AssociationStore::checkNotRevoked); + + synchronized (mLock) { + mIdMap.clear(); + mAddressMap.clear(); + mCachedPerUser.clear(); + + for (AssociationInfo association : associations) { + final int id = association.getId(); + mIdMap.put(id, association); + + final MacAddress address = association.getDeviceMacAddress(); + if (address != null) { + mAddressMap.computeIfAbsent(address, it -> new HashSet<>()).add(id); + } } } } + + private static void checkNotRevoked(@NonNull AssociationInfo association) { + if (association.isRevoked()) { + throw new IllegalArgumentException( + "Revoked (removed) associations MUST NOT appear in the AssociationStore"); + } + } } diff --git a/services/companion/java/com/android/server/companion/association/Associations.java b/services/companion/java/com/android/server/companion/association/Associations.java deleted file mode 100644 index 7da3699dba8d..000000000000 --- a/services/companion/java/com/android/server/companion/association/Associations.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion.association; - -import android.companion.AssociationInfo; - -import java.util.ArrayList; -import java.util.List; - -/** - * Represents associations per user. Should be only used by Association stores. - */ -public class Associations { - - private int mVersion = 0; - - private List<AssociationInfo> mAssociations = new ArrayList<>(); - - private int mMaxId = 0; - - public Associations() { - } - - public void setVersion(int version) { - mVersion = version; - } - - /** - * Add an association. - */ - public void addAssociation(AssociationInfo association) { - mAssociations.add(association); - } - - public void setMaxId(int maxId) { - mMaxId = maxId; - } - - public void setAssociations(List<AssociationInfo> associations) { - mAssociations = List.copyOf(associations); - } - - public int getVersion() { - return mVersion; - } - - public int getMaxId() { - return mMaxId; - } - - public List<AssociationInfo> getAssociations() { - return mAssociations; - } -} diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java deleted file mode 100644 index ec8977918c56..000000000000 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion.association; - -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; - -import static com.android.internal.util.CollectionUtils.any; -import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.annotation.UserIdInt; -import android.app.ActivityManager; -import android.companion.AssociationInfo; -import android.content.Context; -import android.content.pm.PackageManagerInternal; -import android.os.Binder; -import android.os.UserHandle; -import android.util.Slog; - -import com.android.server.companion.CompanionApplicationController; -import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; -import com.android.server.companion.transport.CompanionTransportManager; - -/** - * A class response for Association removal. - */ -@SuppressLint("LongLogTag") -public class DisassociationProcessor { - - private static final String TAG = "CDM_DisassociationProcessor"; - @NonNull - private final Context mContext; - @NonNull - private final AssociationStore mAssociationStore; - @NonNull - private final PackageManagerInternal mPackageManagerInternal; - @NonNull - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; - @NonNull - private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; - @NonNull - private final CompanionApplicationController mCompanionAppController; - @NonNull - private final CompanionTransportManager mTransportManager; - private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; - private final ActivityManager mActivityManager; - - public DisassociationProcessor(@NonNull Context context, - @NonNull ActivityManager activityManager, - @NonNull AssociationStore associationStore, - @NonNull PackageManagerInternal packageManager, - @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, - @NonNull CompanionApplicationController applicationController, - @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, - @NonNull CompanionTransportManager companionTransportManager) { - mContext = context; - mActivityManager = activityManager; - mAssociationStore = associationStore; - mPackageManagerInternal = packageManager; - mOnPackageVisibilityChangeListener = - new OnPackageVisibilityChangeListener(); - mDevicePresenceMonitor = devicePresenceMonitor; - mCompanionAppController = applicationController; - mSystemDataTransferRequestStore = systemDataTransferRequestStore; - mTransportManager = companionTransportManager; - } - - /** - * Disassociate an association by id. - */ - // TODO: also revoke notification access - public void disassociate(int id) { - Slog.i(TAG, "Disassociating id=[" + id + "]..."); - - final AssociationInfo association = mAssociationStore.getAssociationById(id); - if (association == null) { - Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist."); - return; - } - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - final String deviceProfile = association.getDeviceProfile(); - - final boolean isRoleInUseByOtherAssociations = deviceProfile != null - && any(mAssociationStore.getActiveAssociationsByPackage(userId, packageName), - it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId()); - - final int packageProcessImportance = getPackageProcessImportance(userId, packageName); - if (packageProcessImportance <= IMPORTANCE_VISIBLE && deviceProfile != null - && !isRoleInUseByOtherAssociations) { - // Need to remove the app from the list of role holders, but the process is visible - // to the user at the moment, so we'll need to do it later. - Slog.i(TAG, "Cannot disassociate id=[" + id + "] now - process is visible. " - + "Start listening to package importance..."); - - AssociationInfo revokedAssociation = (new AssociationInfo.Builder( - association)).setRevoked(true).build(); - mAssociationStore.updateAssociation(revokedAssociation); - startListening(); - return; - } - - // Association cleanup. - mAssociationStore.removeAssociation(association.getId()); - mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - - // Detach transport if exists - mTransportManager.detachSystemDataTransport(packageName, userId, id); - - // If role is not in use by other associations, revoke the role. - // Do not need to remove the system role since it was pre-granted by the system. - if (!isRoleInUseByOtherAssociations && deviceProfile != null && !deviceProfile.equals( - DEVICE_PROFILE_AUTOMOTIVE_PROJECTION)) { - removeRoleHolderForAssociation(mContext, association.getUserId(), - association.getPackageName(), association.getDeviceProfile()); - } - - // Unbind the app if needed. - final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(id); - if (!wasPresent || !association.isNotifyOnDeviceNearby()) { - return; - } - final boolean shouldStayBound = any( - mAssociationStore.getActiveAssociationsByPackage(userId, packageName), - it -> it.isNotifyOnDeviceNearby() - && mDevicePresenceMonitor.isDevicePresent(it.getId())); - if (!shouldStayBound) { - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } - } - - @SuppressLint("MissingPermission") - private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { - return Binder.withCleanCallingIdentity(() -> { - final int uid = - mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId); - return mActivityManager.getUidImportance(uid); - }); - } - - private void startListening() { - Slog.i(TAG, "Start listening to uid importance changes..."); - try { - Binder.withCleanCallingIdentity( - () -> mActivityManager.addOnUidImportanceListener( - mOnPackageVisibilityChangeListener, - ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); - } catch (IllegalArgumentException e) { - Slog.e(TAG, "Failed to start listening to uid importance changes."); - } - } - - private void stopListening() { - Slog.i(TAG, "Stop listening to uid importance changes."); - try { - Binder.withCleanCallingIdentity(() -> mActivityManager.removeOnUidImportanceListener( - mOnPackageVisibilityChangeListener)); - } catch (IllegalArgumentException e) { - Slog.e(TAG, "Failed to stop listening to uid importance changes."); - } - } - - /** - * An OnUidImportanceListener class which watches the importance of the packages. - * In this class, we ONLY interested in the importance of the running process is greater than - * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}. - * - * Lastly remove the role holder for the revoked associations for the same packages. - * - * @see #disassociate(int) - */ - private class OnPackageVisibilityChangeListener implements - ActivityManager.OnUidImportanceListener { - - @Override - public void onUidImportance(int uid, int importance) { - if (importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) { - // The lower the importance value the more "important" the process is. - // We are only interested when the process ceases to be visible. - return; - } - - final String packageName = mPackageManagerInternal.getNameForUid(uid); - if (packageName == null) { - // Not interested in this uid. - return; - } - - int userId = UserHandle.getUserId(uid); - for (AssociationInfo association : mAssociationStore.getRevokedAssociations(userId, - packageName)) { - disassociate(association.getId()); - } - - if (mAssociationStore.getRevokedAssociations().isEmpty()) { - stopListening(); - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java index f28731548dcc..894c49a2b5cf 100644 --- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -33,7 +33,7 @@ import com.android.server.companion.CompanionDeviceManagerServiceInternal; * A Job Service responsible for clean up idle self-managed associations. * * The job will be executed only if the device is charging and in idle mode due to the application - * will be killed if association/role are revoked. See {@link DisassociationProcessor} + * will be killed if association/role are revoked. See {@link AssociationRevokeProcessor} */ public class InactiveAssociationsRemovalService extends JobService { diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index c5ca0bf7e9c5..a08e0da90d49 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -186,14 +186,18 @@ public class SystemDataTransferProcessor { intent.putExtras(extras); // Create a PendingIntent - return Binder.withCleanCallingIdentity(() -> - PendingIntent.getActivityAsUser(mContext, /*requestCode */ associationId, - intent, FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, - ActivityOptions.makeBasic() - .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - .toBundle(), - UserHandle.CURRENT)); + final long token = Binder.clearCallingIdentity(); + try { + return PendingIntent.getActivityAsUser(mContext, /*requestCode */ associationId, intent, + FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(), + UserHandle.CURRENT); + } finally { + Binder.restoreCallingIdentity(token); + } } /** @@ -224,7 +228,8 @@ public class SystemDataTransferProcessor { } // Start permission sync - Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { // TODO: refactor to work with streams of data mPermissionControllerManager.getRuntimePermissionBackup(UserHandle.of(userId), mExecutor, backup -> { @@ -232,31 +237,39 @@ public class SystemDataTransferProcessor { .requestPermissionRestore(associationId, backup); translateFutureToCallback(future, callback); }); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } /** * Enable perm sync for the association */ public void enablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { int userId = mAssociationStore.getAssociationById(associationId).getUserId(); PermissionSyncRequest request = new PermissionSyncRequest(associationId); request.setUserConsented(true); mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } /** * Disable perm sync for the association */ public void disablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { int userId = mAssociationStore.getAssociationById(associationId).getUserId(); PermissionSyncRequest request = new PermissionSyncRequest(associationId); request.setUserConsented(false); mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } /** @@ -264,7 +277,8 @@ public class SystemDataTransferProcessor { */ @Nullable public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - return Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { int userId = mAssociationStore.getAssociationById(associationId).getUserId(); List<SystemDataTransferRequest> requests = mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, @@ -275,17 +289,22 @@ public class SystemDataTransferProcessor { } } return null; - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } /** * Remove perm sync request for the association. */ public void removePermissionSyncRequest(int associationId) { - Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { int userId = mAssociationStore.getAssociationById(associationId).getUserId(); mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, associationId); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } private void onReceivePermissionRestore(byte[] message) { @@ -299,12 +318,14 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Applying permissions."); // Start applying permissions UserHandle user = mContext.getUser(); - - Binder.withCleanCallingIdentity(() -> { + final long callingIdentityToken = Binder.clearCallingIdentity(); + try { // TODO: refactor to work with streams of data mPermissionControllerManager.stageAndApplyRuntimePermissionsBackup( message, user); - }); + } finally { + Binder.restoreCallingIdentity(callingIdentityToken); + } } private static void translateFutureToCallback(@NonNull Future<?> future, diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index c89ce11c169d..99466a966647 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -106,7 +106,7 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { checkBleState(); registerBluetoothStateBroadcastReceiver(context); - mAssociationStore.registerLocalListener(this); + mAssociationStore.registerListener(this); } @MainThread @@ -183,7 +183,7 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { // Collect MAC addresses from all associations. final Set<String> macAddresses = new HashSet<>(); - for (AssociationInfo association : mAssociationStore.getActiveAssociations()) { + for (AssociationInfo association : mAssociationStore.getAssociations()) { if (!association.isNotifyOnDeviceNearby()) continue; // Beware that BT stack does not consider low-case MAC addresses valid, while @@ -255,7 +255,7 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { if (DEBUG) Log.i(TAG, "notifyDevice_Found()" + btDeviceToString(device)); final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByAddress(device.getAddress()); + mAssociationStore.getAssociationsByAddress(device.getAddress()); if (DEBUG) Log.d(TAG, " > associations=" + Arrays.toString(associations.toArray())); for (AssociationInfo association : associations) { @@ -268,7 +268,7 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { if (DEBUG) Log.i(TAG, "notifyDevice_Lost()" + btDeviceToString(device)); final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByAddress(device.getAddress()); + mAssociationStore.getAssociationsByAddress(device.getAddress()); if (DEBUG) Log.d(TAG, " > associations=" + Arrays.toString(associations.toArray())); for (AssociationInfo association : associations) { @@ -319,7 +319,7 @@ class BleCompanionDeviceScanner implements AssociationStore.OnChangeListener { Log.v(TAG, " > scanResult=" + result); final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByAddress(device.getAddress()); + mAssociationStore.getAssociationsByAddress(device.getAddress()); Log.v(TAG, " > associations=" + Arrays.toString(associations.toArray())); } diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index cb363a7c9d7f..4da3f9bead4e 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -93,7 +93,7 @@ public class BluetoothCompanionDeviceConnectionListener btAdapter.registerBluetoothConnectionCallback( new HandlerExecutor(Handler.getMain()), /* callback */this); - mAssociationStore.registerLocalListener(this); + mAssociationStore.registerListener(this); } /** @@ -168,7 +168,7 @@ public class BluetoothCompanionDeviceConnectionListener private void onDeviceConnectivityChanged(@NonNull BluetoothDevice device, boolean connected) { int userId = UserHandle.myUserId(); final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByAddress(device.getAddress()); + mAssociationStore.getAssociationsByAddress(device.getAddress()); final List<ObservableUuid> observableUuids = mObservableUuidStore.getObservableUuidsForUser(userId); final ParcelUuid[] bluetoothDeviceUuids = device.getUuids(); diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java index 7a1a83f53315..37bbb937d1b5 100644 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java @@ -145,7 +145,7 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange Log.w(TAG, "BluetoothAdapter is NOT available."); } - mAssociationStore.registerLocalListener(this); + mAssociationStore.registerListener(this); } /** @@ -481,7 +481,7 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange * BT connected and BLE presence and are not pending to report BLE lost. */ private boolean canStopBleScan() { - for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { + for (AssociationInfo ai : mAssociationStore.getAssociations()) { int id = ai.getId(); synchronized (mBtDisconnectedDevices) { if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java index db15da2922cf..ee8b1065b42c 100644 --- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java +++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java @@ -90,8 +90,6 @@ public class ObservableUuidStore { * Remove the observable uuid. */ public void removeObservableUuid(@UserIdInt int userId, ParcelUuid uuid, String packageName) { - Slog.i(TAG, "Removing uuid=[" + uuid.getUuid() + "] from store..."); - List<ObservableUuid> cachedObservableUuids; synchronized (mLock) { @@ -110,7 +108,7 @@ public class ObservableUuidStore { * Write the observable uuid. */ public void writeObservableUuid(@UserIdInt int userId, ObservableUuid uuid) { - Slog.i(TAG, "Writing uuid=[" + uuid.getUuid() + "] to store..."); + Slog.i(TAG, "Writing uuid=" + uuid.getUuid() + " to store."); List<ObservableUuid> cachedObservableUuids; synchronized (mLock) { diff --git a/services/companion/java/com/android/server/companion/utils/DataStoreUtils.java b/services/companion/java/com/android/server/companion/utils/DataStoreUtils.java index 369a92504948..c75b1a57206e 100644 --- a/services/companion/java/com/android/server/companion/utils/DataStoreUtils.java +++ b/services/companion/java/com/android/server/companion/utils/DataStoreUtils.java @@ -64,8 +64,8 @@ public final class DataStoreUtils { * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it * possible to synchronize reads and writes to the file using the returned object. * - * @param userId the userId to retrieve the storage file - * @param fileName the storage file name + * @param userId the userId to retrieve the storage file + * @param fileName the storage file name * @return an AtomicFile for the user */ @NonNull diff --git a/services/companion/java/com/android/server/companion/utils/RolesUtils.java b/services/companion/java/com/android/server/companion/utils/RolesUtils.java index dd12e0406089..f798e218e8e0 100644 --- a/services/companion/java/com/android/server/companion/utils/RolesUtils.java +++ b/services/companion/java/com/android/server/companion/utils/RolesUtils.java @@ -93,8 +93,8 @@ public final class RolesUtils { Slog.i(TAG, "Removing CDM role=" + deviceProfile + " for userId=" + userId + ", packageName=" + packageName); - - Binder.withCleanCallingIdentity(() -> + final long identity = Binder.clearCallingIdentity(); + try { roleManager.removeRoleHolderAsUser(deviceProfile, packageName, MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(), success -> { @@ -103,9 +103,11 @@ public final class RolesUtils { + packageName + " from the list of " + deviceProfile + " holders."); } - }) - ); + }); + } finally { + Binder.restoreCallingIdentity(identity); + } } - private RolesUtils() {} + private RolesUtils() {}; } |