Move association revoke logic to AssociationRevokeProcessor

Bug: 318413151
Test: CDM CTS tests
Change-Id: I45037b26602561516fc68480f90a66e2180d3a7d
diff --git a/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java b/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java
new file mode 100644
index 0000000..de6382e
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/AssociationRevokeProcessor.java
@@ -0,0 +1,369 @@
+/*
+ * 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;
+
+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.MetricUtils.logRemoveAssociation;
+import static com.android.server.companion.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.datatransfer.SystemDataTransferRequestStore;
+import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+
+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 AssociationStoreImpl mAssociationStore;
+    private final @NonNull PackageManagerInternal mPackageManagerInternal;
+    private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
+    private final @NonNull SystemDataTransferRequestStore mSystemDataTransferRequestStore;
+    private final @NonNull CompanionApplicationController mCompanionAppController;
+    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<>();
+
+    AssociationRevokeProcessor(@NonNull CompanionDeviceManagerService service,
+            @NonNull AssociationStoreImpl associationStore,
+            @NonNull PackageManagerInternal packageManager,
+            @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor,
+            @NonNull CompanionApplicationController applicationController,
+            @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore) {
+        mService = service;
+        mContext = service.getContext();
+        mActivityManager = mContext.getSystemService(ActivityManager.class);
+        mAssociationStore = associationStore;
+        mPackageManagerInternal = packageManager;
+        mOnPackageVisibilityChangeListener =
+                new OnPackageVisibilityChangeListener(mActivityManager);
+        mDevicePresenceMonitor = devicePresenceMonitor;
+        mCompanionAppController = applicationController;
+        mSystemDataTransferRequestStore = systemDataTransferRequestStore;
+    }
+
+    // TODO: also revoke notification access
+    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();
+
+        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.
+     */
+    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);
+        return true;
+    }
+
+    @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);
+        });
+    }
+
+    /**
+     * Set revoked flag for active association and add the revoked association and the uid into
+     * the caches.
+     *
+     * @see #mRevokedAssociationsPendingRoleHolderRemoval
+     * @see #mUidsPendingRoleHolderRemoval
+     * @see OnPackageVisibilityChangeListener
+     */
+    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();
+                }
+            }
+        }
+    }
+
+    /**
+     * 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();
+            }
+        }
+    }
+
+    /**
+     * @return a copy of the revoked associations set (safeguarding against
+     *         {@code ConcurrentModificationException}-s).
+     */
+    @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
+            @UserIdInt int userId) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            // Return a copy.
+            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
+        }
+    }
+
+    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/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index ba1f51b..09c7793 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -22,7 +22,6 @@
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
 import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
-import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
 import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
 import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
@@ -39,7 +38,6 @@
 import static com.android.internal.util.Preconditions.checkState;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 import static com.android.server.companion.AssociationStore.CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED;
-import static com.android.server.companion.MetricUtils.logRemoveAssociation;
 import static com.android.server.companion.PackageUtils.isRestrictedSettingsAllowed;
 import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
 import static com.android.server.companion.PackageUtils.getPackageInfo;
@@ -49,7 +47,6 @@
 import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
 import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
-import static com.android.server.companion.RolesUtils.removeRoleHolderForAssociation;
 
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.DAYS;
@@ -61,7 +58,6 @@
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
-import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
@@ -149,7 +145,7 @@
     static final String TAG = "CDM_CompanionDeviceManagerService";
     static final boolean DEBUG = false;
 
-    /** Range of Association IDs allocated for a user.*/
+    /** Range of Association IDs allocated for a user. */
     private static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000;
     private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
 
@@ -162,8 +158,6 @@
     private static final int MAX_CN_LENGTH = 500;
 
     private final ActivityManager mActivityManager;
-    private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
-
     private PersistentDataStore mPersistentStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
 
@@ -175,6 +169,7 @@
     private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private CompanionApplicationController mCompanionAppController;
     private CompanionTransportManager mTransportManager;
+    private AssociationRevokeProcessor mAssociationRevokeProcessor;
 
     private final ActivityTaskManagerInternal mAtmInternal;
     private final ActivityManagerInternal mAmInternal;
@@ -193,33 +188,6 @@
     @GuardedBy("mPreviouslyUsedIds")
     private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>();
 
-    /**
-     * 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<>();
-
     private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners =
             new RemoteCallbackList<>();
 
@@ -243,8 +211,6 @@
         mAssociationStore = new AssociationStoreImpl();
         mSystemDataTransferRequestStore = new SystemDataTransferRequestStore();
 
-        mOnPackageVisibilityChangeListener =
-                new OnPackageVisibilityChangeListener(mActivityManager);
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
         mObservableUuidStore = new ObservableUuidStore();
     }
@@ -276,6 +242,9 @@
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                 mPackageManagerInternal, mAssociationStore,
                 mSystemDataTransferRequestStore, mTransportManager);
+        mAssociationRevokeProcessor = new AssociationRevokeProcessor(this, mAssociationStore,
+                mPackageManagerInternal, mDevicePresenceMonitor, mCompanionAppController,
+                mSystemDataTransferRequestStore);
         // TODO(b/279663946): move context sync to a dedicated system service
         mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
 
@@ -307,12 +276,13 @@
                 mBackupRestoreProcessor.addToPendingAppInstall(association);
             } else if (!association.isRevoked()) {
                 activeAssociations.add(association);
-            } else if (maybeRemoveRoleHolderForAssociation(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 {
-                addToPendingRoleHolderRemoval(association);
+                mAssociationRevokeProcessor.addToPendingRoleHolderRemoval(association);
             }
         }
 
@@ -374,7 +344,7 @@
                 final List<ParcelUuid> deviceUuids = ArrayUtils.isEmpty(bluetoothDeviceUuids)
                         ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids);
 
-                for (AssociationInfo ai:
+                for (AssociationInfo ai :
                         mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) {
                     Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
                     mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId());
@@ -495,7 +465,7 @@
         final String packageName = uuid.getPackageName();
         final int userId = uuid.getUserId();
 
-        switch(event) {
+        switch (event) {
             case EVENT_BT_CONNECTED:
                 if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
                     mCompanionAppController.bindCompanionApplication(
@@ -544,8 +514,8 @@
 
     /**
      * @return whether the package should be bound (i.e. at least one of the devices associated with
-     *         the package is currently present OR the UUID to be observed by this package is
-     *         currently present).
+     * the package is currently present OR the UUID to be observed by this package is
+     * currently present).
      */
     private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
         final List<AssociationInfo> packageAssociations =
@@ -599,7 +569,8 @@
         allAssociations = new ArrayList<>(
                 mAssociationStore.getAssociationsForUser(userId));
         // ... and add the revoked (removed) association, that are yet to be permanently removed.
-        allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId));
+        allAssociations.addAll(
+                mAssociationRevokeProcessor.getPendingRoleHolderRemovalAssociationsForUser(userId));
         // ... and add the restored associations that are pending missing package installation.
         allAssociations.addAll(mBackupRestoreProcessor
                 .getAssociationsPendingAppInstallForUser(userId));
@@ -654,7 +625,7 @@
         }
         // Clear role holders
         for (AssociationInfo association : associationsForPackage) {
-            maybeRemoveRoleHolderForAssociation(association);
+            mAssociationRevokeProcessor.maybeRemoveRoleHolderForAssociation(association);
         }
         // Clear the uuids to be observed.
         for (ObservableUuid uuid : uuidsTobeObserved) {
@@ -712,7 +683,7 @@
             final int id = association.getId();
 
             Slog.i(TAG, "Removing inactive self-managed association id=" + id);
-            disassociateInternal(id);
+            mAssociationRevokeProcessor.disassociateInternal(id);
         }
     }
 
@@ -857,7 +828,7 @@
 
             final AssociationInfo association =
                     getAssociationWithCallerChecks(userId, packageName, deviceMacAddress);
-            disassociateInternal(association.getId());
+            mAssociationRevokeProcessor.disassociateInternal(association.getId());
         }
 
         @Override
@@ -866,7 +837,7 @@
 
             final AssociationInfo association =
                     getAssociationWithCallerChecks(associationId);
-            disassociateInternal(association.getId());
+            mAssociationRevokeProcessor.disassociateInternal(association.getId());
         }
 
         @Override
@@ -902,9 +873,9 @@
         }
 
         /**
-        * @deprecated Use
-        * {@link NotificationManager#isNotificationListenerAccessGranted(ComponentName)} instead.
-        */
+         * @deprecated Use
+         * {@link NotificationManager#isNotificationListenerAccessGranted(ComponentName)} instead.
+         */
         @Deprecated
         @Override
         public boolean hasNotificationAccess(ComponentName component) throws RemoteException {
@@ -1300,7 +1271,7 @@
             return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this,
                     mAssociationStore, mDevicePresenceMonitor, mTransportManager,
                     mSystemDataTransferProcessor, mAssociationRequestsProcessor,
-                    mBackupRestoreProcessor)
+                    mBackupRestoreProcessor, mAssociationRevokeProcessor)
                     .exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
                             err.getFileDescriptor(), args);
         }
@@ -1381,7 +1352,7 @@
             // another association by the time when it is activated from the package installation.
             final Set<AssociationInfo> pendingAssociations = mBackupRestoreProcessor
                     .getAssociationsPendingAppInstallForUser(userId);
-            for (AssociationInfo it: pendingAssociations) {
+            for (AssociationInfo it : pendingAssociations) {
                 usedIds.put(it.getId(), true);
             }
 
@@ -1407,198 +1378,6 @@
         }
     }
 
-    // TODO: also revoke notification access
-    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();
-
-        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.
-     */
-
-    private 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(getContext(), association);
-        return true;
-    }
-
-    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);
-        });
-    }
-
-    /**
-     * Set revoked flag for active association and add the revoked association and the uid into
-     * the caches.
-     *
-     * @see #mRevokedAssociationsPendingRoleHolderRemoval
-     * @see #mUidsPendingRoleHolderRemoval
-     * @see OnPackageVisibilityChangeListener
-     */
-    private 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();
-                }
-            }
-        }
-    }
-
-    /**
-     * 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();
-            }
-        }
-    }
-
-    /**
-     * @return a copy of the revoked associations set (safeguarding against
-     *         {@code ConcurrentModificationException}-s).
-     */
-    private @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
-            @UserIdInt int userId) {
-        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
-            // Return a copy.
-            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
-        }
-    }
-
-    private String getPackageNameByUid(int uid) {
-        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
-            return mUidsPendingRoleHolderRemoval.get(uid);
-        }
-    }
-
     void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
         final PackageInfo packageInfo =
                 getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
@@ -1704,11 +1483,11 @@
 
     private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
             new AssociationStore.OnChangeListener() {
-        @Override
-        public void onAssociationChanged(int changeType, AssociationInfo association) {
-            onAssociationChangedInternal(changeType, association);
-        }
-    };
+                @Override
+                public void onAssociationChanged(int changeType, AssociationInfo association) {
+                    onAssociationChangedInternal(changeType, association);
+                }
+            };
 
     private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
             new CompanionDevicePresenceMonitor.Callback() {
@@ -1731,7 +1510,7 @@
                 public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
                     onDevicePresenceEventByUuidInternal(uuid, event);
                 }
-    };
+            };
 
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
@@ -1887,73 +1666,8 @@
         }
     }
 
-    /**
-     * 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 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;
-        }
-
-        void startListening() {
-            Binder.withCleanCallingIdentity(
-                    () -> mAm.addOnUidImportanceListener(
-                            /* listener */ OnPackageVisibilityChangeListener.this,
-                            RunningAppProcessInfo.IMPORTANCE_VISIBLE));
-        }
-
-        void stopListening() {
-            Binder.withCleanCallingIdentity(
-                    () -> mAm.removeOnUidImportanceListener(
-                            /* listener */ OnPackageVisibilityChangeListener.this));
-        }
-
-        @Override
-        public void onUidImportance(int uid, int importance) {
-            if (importance <= 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) {
-                mUserPersistenceHandler.postPersistUserState(userId);
-            }
-        }
+    void postPersistUserState(@UserIdInt int userId) {
+        mUserPersistenceHandler.postPersistUserState(userId);
     }
 
     static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 5663434..de4f2b6 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -45,6 +45,7 @@
     private static final String TAG = "CDM_CompanionDeviceShellCommand";
 
     private final CompanionDeviceManagerService mService;
+    private final AssociationRevokeProcessor mRevokeProcessor;
     private final AssociationStoreImpl mAssociationStore;
     private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private final CompanionTransportManager mTransportManager;
@@ -59,7 +60,8 @@
             CompanionTransportManager transportManager,
             SystemDataTransferProcessor systemDataTransferProcessor,
             AssociationRequestsProcessor associationRequestsProcessor,
-            BackupRestoreProcessor backupRestoreProcessor) {
+            BackupRestoreProcessor backupRestoreProcessor,
+            AssociationRevokeProcessor revokeProcessor) {
         mService = service;
         mAssociationStore = associationStore;
         mDevicePresenceMonitor = devicePresenceMonitor;
@@ -67,6 +69,7 @@
         mSystemDataTransferProcessor = systemDataTransferProcessor;
         mAssociationRequestsProcessor = associationRequestsProcessor;
         mBackupRestoreProcessor = backupRestoreProcessor;
+        mRevokeProcessor = revokeProcessor;
     }
 
     @Override
@@ -126,7 +129,7 @@
                     final AssociationInfo association =
                             mService.getAssociationWithCallerChecks(userId, packageName, address);
                     if (association != null) {
-                        mService.disassociateInternal(association.getId());
+                        mRevokeProcessor.disassociateInternal(association.getId());
                     }
                 }
                 break;
@@ -138,7 +141,7 @@
                             mAssociationStore.getAssociationsForPackage(userId, packageName);
                     for (AssociationInfo association : userAssociations) {
                         if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
-                            mService.disassociateInternal(association.getId());
+                            mRevokeProcessor.disassociateInternal(association.getId());
                         }
                     }
                 }