Introduce CDM AssociationRequestsProcessor

Moving the code for handling incoming AssociationRequests from
CompanionDeviceManagerServices into a separate class -
AssociationRequestsProcessor.
AssociationRequestsProcessor is reponsible for:
- validating the incoming requests
- making a decision whether user confirmation is required
- launching and communicating with the UI (if needed)

Bug: 197933995
Bug: 194100881
Test: make
Change-Id: I8f0694c44b24697db2917a8ab06a6abceff315e3
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
new file mode 100644
index 0000000..2a0adeb
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2021 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.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.internal.util.CollectionUtils.filter;
+import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
+import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
+import static com.android.server.companion.CompanionDeviceManagerService.getCallingUserId;
+
+import static java.util.Collections.unmodifiableMap;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.companion.Association;
+import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
+import android.companion.ICompanionDeviceDiscoveryService;
+import android.companion.IFindDeviceCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.Signature;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.PackageUtils;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.PerUser;
+import com.android.internal.infra.ServiceConnector;
+import com.android.internal.util.ArrayUtils;
+import com.android.server.FgThread;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+class AssociationRequestsProcessor {
+    private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor";
+
+    private static final Map<String, String> DEVICE_PROFILE_TO_PERMISSION;
+    static {
+        final Map<String, String> map = new ArrayMap<>();
+        map.put(DEVICE_PROFILE_WATCH, Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH);
+        map.put(DEVICE_PROFILE_APP_STREAMING,
+                Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING);
+
+        DEVICE_PROFILE_TO_PERMISSION = unmodifiableMap(map);
+    }
+
+    private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
+            CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
+            ".CompanionDeviceDiscoveryService");
+
+    private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
+    private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
+
+    private final Context mContext;
+    private final CompanionDeviceManagerService mService;
+
+    private AssociationRequest mRequest;
+    private IFindDeviceCallback mFindDeviceCallback;
+    private String mCallingPackage;
+    private AndroidFuture<?> mOngoingDeviceDiscovery;
+
+    private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
+
+    AssociationRequestsProcessor(CompanionDeviceManagerService service) {
+        mContext = service.getContext();
+        mService = service;
+
+        final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
+        mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
+            @Override
+            protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
+                return new ServiceConnector.Impl<>(
+                        mContext,
+                        serviceIntent, 0/* bindingFlags */, userId,
+                        ICompanionDeviceDiscoveryService.Stub::asInterface);
+            }
+        };
+    }
+
+    void process(AssociationRequest request, IFindDeviceCallback callback, String callingPackage)
+            throws RemoteException {
+        if (DEBUG) {
+            Slog.d(TAG, "process(request=" + request + ", from=" + callingPackage + ")");
+        }
+
+        checkNotNull(request, "Request cannot be null");
+        checkNotNull(callback, "Callback cannot be null");
+        mService.checkCallerIsSystemOr(callingPackage);
+        int userId = getCallingUserId();
+        mService.checkUsesFeature(callingPackage, userId);
+        final String deviceProfile = request.getDeviceProfile();
+        validateDeviceProfileAndCheckPermission(deviceProfile);
+
+        mFindDeviceCallback = callback;
+        mRequest = request;
+        mCallingPackage = callingPackage;
+        request.setCallingPackage(callingPackage);
+
+        if (mayAssociateWithoutPrompt(callingPackage, userId)) {
+            Slog.i(TAG, "setSkipPrompt(true)");
+            request.setSkipPrompt(true);
+        }
+        callback.asBinder().linkToDeath(mBinderDeathRecipient /* recipient */, 0);
+
+        mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
+                .thenComposeAsync(description -> {
+                    if (DEBUG) {
+                        Slog.d(TAG, "fetchProfileDescription done: " + description);
+                    }
+
+                    request.setDeviceProfilePrivilegesDescription(description);
+
+                    return mServiceConnectors.forUser(userId).postAsync(service -> {
+                        if (DEBUG) {
+                            Slog.d(TAG, "Connected to CDM service -> "
+                                    + "Starting discovery for " + request);
+                        }
+
+                        AndroidFuture<String> future = new AndroidFuture<>();
+                        service.startDiscovery(request, callingPackage, callback, future);
+                        return future;
+                    }).cancelTimeout();
+
+                }, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> {
+                    if (err == null) {
+                        mService.createAssociationInternal(
+                                userId, deviceAddress, callingPackage, deviceProfile);
+                    } else {
+                        Slog.e(TAG, "Failed to discover device(s)", err);
+                        callback.onFailure("No devices found: " + err.getMessage());
+                    }
+                    cleanup();
+                }));
+    }
+
+    void stopScan(AssociationRequest request, IFindDeviceCallback callback, String callingPackage) {
+        if (DEBUG) {
+            Slog.d(TAG, "stopScan(request = " + request + ")");
+        }
+        if (Objects.equals(request, mRequest)
+                && Objects.equals(callback, mFindDeviceCallback)
+                && Objects.equals(callingPackage, mCallingPackage)) {
+            cleanup();
+        }
+    }
+
+    private void validateDeviceProfileAndCheckPermission(@Nullable String deviceProfile) {
+        // Device profile can be null.
+        if (deviceProfile == null) return;
+
+        if (DEVICE_PROFILE_APP_STREAMING.equals(deviceProfile)) {
+            // TODO: remove, when properly supporting this profile.
+            throw new UnsupportedOperationException(
+                    "DEVICE_PROFILE_APP_STREAMING is not fully supported yet.");
+        }
+
+        if (!DEVICE_PROFILE_TO_PERMISSION.containsKey(deviceProfile)) {
+            throw new IllegalArgumentException("Unsupported device profile: " + deviceProfile);
+        }
+
+        final String permission = DEVICE_PROFILE_TO_PERMISSION.get(deviceProfile);
+        if (mContext.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
+            throw new SecurityException("Application must hold " + permission + " to associate "
+                    + "with a device with " + deviceProfile + " profile.");
+        }
+    }
+
+    private void cleanup() {
+        if (DEBUG) {
+            Slog.d(TAG, "cleanup(); discovery = "
+                    + mOngoingDeviceDiscovery + ", request = " + mRequest);
+        }
+        synchronized (mService.mLock) {
+            AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
+            if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
+                ongoingDeviceDiscovery.cancel(true);
+            }
+            if (mFindDeviceCallback != null) {
+                mFindDeviceCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0);
+                mFindDeviceCallback = null;
+            }
+            mRequest = null;
+            mCallingPackage = null;
+        }
+    }
+
+    private boolean mayAssociateWithoutPrompt(String packageName, int userId) {
+        String[] sameOemPackages = mContext.getResources()
+                .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
+        if (!ArrayUtils.contains(sameOemPackages, packageName)) {
+            Slog.w(TAG, packageName
+                    + " can not silently create associations due to no package found."
+                    + " Packages from OEM: " + Arrays.toString(sameOemPackages)
+            );
+            return false;
+        }
+
+        // Throttle frequent associations
+        long now = System.currentTimeMillis();
+        Set<Association> recentAssociations = filter(
+                mService.getAllAssociations(userId, packageName),
+                a -> now - a.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS);
+
+        if (recentAssociations.size() >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
+            Slog.w(TAG, "Too many associations. " + packageName
+                    + " already associated " + recentAssociations.size()
+                    + " devices within the last " + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS
+                    + "ms: " + recentAssociations);
+            return false;
+        }
+        String[] sameOemCerts = mContext.getResources()
+                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
+
+        Signature[] signatures = mService.mPackageManagerInternal
+                .getPackage(packageName).getSigningDetails().getSignatures();
+        String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
+
+        Set<String> sameOemPackageCerts =
+                getSameOemPackageCerts(packageName, sameOemPackages, sameOemCerts);
+
+        for (String cert : apkCerts) {
+            if (sameOemPackageCerts.contains(cert)) {
+                return true;
+            }
+        }
+
+        Slog.w(TAG, packageName
+                + " can not silently create associations. " + packageName
+                + " has SHA256 certs from APK: " + Arrays.toString(apkCerts)
+                + " and from OEM: " + Arrays.toString(sameOemCerts)
+        );
+
+        return false;
+    }
+
+    @NonNull
+    private AndroidFuture<String> getDeviceProfilePermissionDescription(
+            @Nullable String deviceProfile) {
+        if (deviceProfile == null) {
+            return AndroidFuture.completedFuture(null);
+        }
+
+        final AndroidFuture<String> result = new AndroidFuture<>();
+        mService.mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
+                deviceProfile, FgThread.getExecutor(), desc -> {
+                    try {
+                        result.complete(String.valueOf(desc));
+                    } catch (Exception e) {
+                        result.completeExceptionally(e);
+                    }
+                });
+        return result;
+    }
+
+
+    void dump(@NonNull PrintWriter pw) {
+        pw.append("Discovery Service State:").append('\n');
+        for (int i = 0, size = mServiceConnectors.size(); i < size; i++) {
+            int userId = mServiceConnectors.keyAt(i);
+            pw.append("  ")
+                    .append("u").append(Integer.toString(userId)).append(": ")
+                    .append(Objects.toString(mServiceConnectors.valueAt(i)))
+                    .append('\n');
+        }
+    }
+
+    private final IBinder.DeathRecipient mBinderDeathRecipient = new IBinder.DeathRecipient() {
+        @Override
+        public void binderDied() {
+            if (DEBUG) {
+                Slog.d(TAG, "binderDied()");
+            }
+            mService.mMainHandler.post(AssociationRequestsProcessor.this::cleanup);
+        }
+    };
+
+    private static Set<String> getSameOemPackageCerts(
+            String packageName, String[] oemPackages, String[] sameOemCerts) {
+        Set<String> sameOemPackageCerts = new HashSet<>();
+
+        // Assume OEM may enter same package name in the parallel string array with
+        // multiple APK certs corresponding to it
+        for (int i = 0; i < oemPackages.length; i++) {
+            if (oemPackages[i].equals(packageName)) {
+                sameOemPackageCerts.add(sameOemCerts[i].replaceAll(":", ""));
+            }
+        }
+
+        return sameOemPackageCerts;
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index a48172b..ad4c35c 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -19,8 +19,6 @@
 
 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
 import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
 import static android.companion.DeviceId.TYPE_MAC_ADDRESS;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -31,7 +29,6 @@
 import static com.android.internal.util.CollectionUtils.find;
 import static com.android.internal.util.CollectionUtils.forEach;
 import static com.android.internal.util.CollectionUtils.map;
-import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
 import static com.android.internal.util.Preconditions.checkArgument;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
@@ -39,13 +36,10 @@
 import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
 
 import static java.util.Collections.emptySet;
-import static java.util.Collections.unmodifiableMap;
 import static java.util.Collections.unmodifiableSet;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
-import android.Manifest;
-import android.annotation.CheckResult;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -64,10 +58,8 @@
 import android.bluetooth.le.ScanSettings;
 import android.companion.Association;
 import android.companion.AssociationRequest;
-import android.companion.CompanionDeviceManager;
 import android.companion.DeviceId;
 import android.companion.DeviceNotAssociatedException;
-import android.companion.ICompanionDeviceDiscoveryService;
 import android.companion.ICompanionDeviceManager;
 import android.companion.IFindDeviceCallback;
 import android.content.BroadcastReceiver;
@@ -81,14 +73,11 @@
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.Signature;
 import android.content.pm.UserInfo;
 import android.net.NetworkPolicyManager;
 import android.os.Binder;
 import android.os.Environment;
 import android.os.Handler;
-import android.os.IBinder;
-import android.os.IInterface;
 import android.os.Parcel;
 import android.os.PowerWhitelistManager;
 import android.os.Process;
@@ -103,7 +92,6 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.ExceptionUtils;
-import android.util.PackageUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -111,9 +99,6 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.content.PackageMonitor;
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.infra.PerUser;
-import com.android.internal.infra.ServiceConnector;
 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
@@ -146,27 +131,13 @@
 
 /** @hide */
 @SuppressLint("LongLogTag")
-public class CompanionDeviceManagerService extends SystemService implements Binder.DeathRecipient {
+public class CompanionDeviceManagerService extends SystemService {
     static final String LOG_TAG = "CompanionDeviceManagerService";
     static final boolean DEBUG = false;
 
-    private static final Map<String, String> DEVICE_PROFILE_TO_PERMISSION;
-    static {
-        final Map<String, String> map = new ArrayMap<>();
-        map.put(DEVICE_PROFILE_WATCH, Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH);
-        map.put(DEVICE_PROFILE_APP_STREAMING,
-                Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING);
-
-        DEVICE_PROFILE_TO_PERMISSION = unmodifiableMap(map);
-    }
-
     /** Range of Association IDs allocated for a user.*/
     static final int ASSOCIATIONS_IDS_PER_USER_RANGE = 100000;
 
-    private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
-            CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
-            ".CompanionDeviceDiscoveryService");
-
     private static final long DEVICE_DISAPPEARED_TIMEOUT_MS = 10 * 1000;
     private static final long DEVICE_DISAPPEARED_UNBIND_TIMEOUT_MS = 10 * 60 * 1000;
 
@@ -177,9 +148,6 @@
     private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
     private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
 
-    private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
-    private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
-
     private static DateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     static {
         sDateFormat.setTimeZone(TimeZone.getDefault());
@@ -188,19 +156,15 @@
     private final CompanionDeviceManagerImpl mImpl;
     // Persistent data store for all Associations.
     private final PersistentDataStore mPersistentDataStore;
+    private final AssociationRequestsProcessor mAssociationRequestsProcessor;
     private PowerWhitelistManager mPowerWhitelistManager;
-    private PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
     private IAppOpsService mAppOpsManager;
     private RoleManager mRoleManager;
     private BluetoothAdapter mBluetoothAdapter;
     private UserManager mUserManager;
 
-    private IFindDeviceCallback mFindDeviceCallback;
     private ScanCallback mBleScanCallback = new BleScanCallback();
-    private AssociationRequest mRequest;
-    private String mCallingPackage;
-    private AndroidFuture<?> mOngoingDeviceDiscovery;
-    private PermissionControllerManager mPermissionControllerManager;
+    PermissionControllerManager mPermissionControllerManager;
 
     private BluetoothDeviceConnectedListener mBluetoothDeviceConnectedListener =
             new BluetoothDeviceConnectedListener();
@@ -212,8 +176,8 @@
     private ArrayMap<String, TriggerDeviceDisappearedRunnable> mTriggerDeviceDisappearedRunnables =
             new ArrayMap<>();
 
-    private final Object mLock = new Object();
-    private final Handler mMainHandler = Handler.getMain();
+    final Object mLock = new Object();
+    final Handler mMainHandler = Handler.getMain();
     private CompanionDevicePresenceController mCompanionDevicePresenceController;
 
     /** Maps a {@link UserIdInt} to a set of associations for the user. */
@@ -236,6 +200,7 @@
         super(context);
         mImpl = new CompanionDeviceManagerImpl();
         mPersistentDataStore = new PersistentDataStore();
+        mAssociationRequestsProcessor = new AssociationRequestsProcessor(this);
 
         mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
         mRoleManager = context.getSystemService(RoleManager.class);
@@ -249,17 +214,6 @@
         mUserManager = context.getSystemService(UserManager.class);
         mCompanionDevicePresenceController = new CompanionDevicePresenceController();
 
-        Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
-        mServiceConnectors = new PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>>() {
-            @Override
-            protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
-                return new ServiceConnector.Impl<>(
-                        getContext(),
-                        serviceIntent, 0/* bindingFlags */, userId,
-                        ICompanionDeviceDiscoveryService.Stub::asInterface);
-            }
-        };
-
         registerPackageMonitor();
     }
 
@@ -357,39 +311,6 @@
         }
     }
 
-    @Override
-    public void binderDied() {
-        Slog.w(LOG_TAG, "binderDied()");
-        mMainHandler.post(this::cleanup);
-    }
-
-    private void cleanup() {
-        Slog.d(LOG_TAG, "cleanup(); discovery = "
-                + mOngoingDeviceDiscovery + ", request = " + mRequest);
-        synchronized (mLock) {
-            AndroidFuture<?> ongoingDeviceDiscovery = mOngoingDeviceDiscovery;
-            if (ongoingDeviceDiscovery != null && !ongoingDeviceDiscovery.isDone()) {
-                ongoingDeviceDiscovery.cancel(true);
-            }
-            mFindDeviceCallback = unlinkToDeath(mFindDeviceCallback, this, 0);
-            mRequest = null;
-            mCallingPackage = null;
-        }
-    }
-
-    /**
-     * Usage: {@code a = unlinkToDeath(a, deathRecipient, flags); }
-     */
-    @Nullable
-    @CheckResult
-    private static <T extends IInterface> T unlinkToDeath(T iinterface,
-            IBinder.DeathRecipient deathRecipient, int flags) {
-        if (iinterface != null) {
-            iinterface.asBinder().unlinkToDeath(deathRecipient, flags);
-        }
-        return null;
-    }
-
     class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
 
         @Override
@@ -410,61 +331,15 @@
                 String callingPackage) throws RemoteException {
             Slog.i(LOG_TAG, "associate(request = " + request + ", callback = " + callback
                     + ", callingPackage = " + callingPackage + ")");
-            checkNotNull(request, "Request cannot be null");
-            checkNotNull(callback, "Callback cannot be null");
-            checkCallerIsSystemOr(callingPackage);
-            int userId = getCallingUserId();
-            checkUsesFeature(callingPackage, userId);
-            final String deviceProfile = request.getDeviceProfile();
-            validateDeviceProfileAndCheckPermission(deviceProfile);
-
-            mFindDeviceCallback = callback;
-            mRequest = request;
-            mCallingPackage = callingPackage;
-            request.setCallingPackage(callingPackage);
-
-            if (mayAssociateWithoutPrompt(callingPackage, userId)) {
-                Slog.i(LOG_TAG, "setSkipPrompt(true)");
-                request.setSkipPrompt(true);
-            }
-            callback.asBinder().linkToDeath(CompanionDeviceManagerService.this /* recipient */, 0);
-
-            mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
-                    .thenComposeAsync(description -> {
-                Slog.d(LOG_TAG, "fetchProfileDescription done: " + description);
-
-                request.setDeviceProfilePrivilegesDescription(description);
-
-                return mServiceConnectors.forUser(userId).postAsync(service -> {
-                    Slog.d(LOG_TAG, "Connected to CDM service; starting discovery for " + request);
-
-                    AndroidFuture<String> future = new AndroidFuture<>();
-                    service.startDiscovery(request, callingPackage, callback, future);
-                    return future;
-                }).cancelTimeout();
-
-            }, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> {
-                if (err == null) {
-                    createAssociationInternal(
-                            userId, deviceAddress, callingPackage, deviceProfile);
-                } else {
-                    Slog.e(LOG_TAG, "Failed to discover device(s)", err);
-                    callback.onFailure("No devices found: " + err.getMessage());
-                }
-                cleanup();
-            }));
+            mAssociationRequestsProcessor.process(request, callback, callingPackage);
         }
 
         @Override
         public void stopScan(AssociationRequest request,
                 IFindDeviceCallback callback,
                 String callingPackage) {
-            Slog.d(LOG_TAG, "stopScan(request = " + request + ")");
-            if (Objects.equals(request, mRequest)
-                    && Objects.equals(callback, mFindDeviceCallback)
-                    && Objects.equals(callingPackage, mCallingPackage)) {
-                cleanup();
-            }
+            Slog.i(LOG_TAG, "stopScan(request = " + request + ")");
+            mAssociationRequestsProcessor.stopScan(request, callback, callingPackage);
         }
 
         @Override
@@ -505,44 +380,6 @@
                     == PERMISSION_GRANTED;
         }
 
-        private void checkCallerIsSystemOr(String pkg) throws RemoteException {
-            checkCallerIsSystemOr(pkg, getCallingUserId());
-        }
-
-        private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
-            if (isCallerSystem()) {
-                return;
-            }
-
-            checkArgument(getCallingUserId() == userId,
-                    "Must be called by either same user or system");
-            int callingUid = Binder.getCallingUid();
-            if (mAppOpsManager.checkPackage(callingUid, pkg) != AppOpsManager.MODE_ALLOWED) {
-                throw new SecurityException(pkg + " doesn't belong to uid " + callingUid);
-            }
-        }
-
-        private void validateDeviceProfileAndCheckPermission(@Nullable String deviceProfile) {
-            // Device profile can be null.
-            if (deviceProfile == null) return;
-
-            if (DEVICE_PROFILE_APP_STREAMING.equals(deviceProfile)) {
-                // TODO: remove, when properly supporting this profile.
-                throw new UnsupportedOperationException(
-                        "DEVICE_PROFILE_APP_STREAMING is not fully supported yet.");
-            }
-
-            if (!DEVICE_PROFILE_TO_PERMISSION.containsKey(deviceProfile)) {
-                throw new IllegalArgumentException("Unsupported device profile: " + deviceProfile);
-            }
-
-            final String permission = DEVICE_PROFILE_TO_PERMISSION.get(deviceProfile);
-            if (getContext().checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
-                throw new SecurityException("Application must hold " + permission + " to associate "
-                        + "with a device with " + deviceProfile + " profile.");
-            }
-        }
-
         @Override
         public PendingIntent requestNotificationAccess(ComponentName component)
                 throws RemoteException {
@@ -674,23 +511,6 @@
             checkUsesFeature(callingPackage, userId);
         }
 
-        private void checkUsesFeature(String pkg, int userId) {
-            if (isCallerSystem()) {
-                // Drop the requirement for calls from system process
-                return;
-            }
-
-            FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
-            String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
-            int numFeatures = ArrayUtils.size(reqFeatures);
-            for (int i = 0; i < numFeatures; i++) {
-                if (requiredFeature.equals(reqFeatures[i].name)) return;
-            }
-            throw new IllegalStateException("Must declare uses-feature "
-                    + requiredFeature
-                    + " in manifest to use this API");
-        }
-
         @Override
         public boolean canPairWithoutPrompt(
                 String packageName, String deviceMacAddress, int userId) {
@@ -740,14 +560,7 @@
                         .append(sDateFormat.format(time)).append('\n');
             }
 
-            fout.append("Discovery Service State:").append('\n');
-            for (int i = 0, size = mServiceConnectors.size(); i < size; i++) {
-                int userId = mServiceConnectors.keyAt(i);
-                fout.append("  ")
-                        .append("u").append(Integer.toString(userId)).append(": ")
-                        .append(Objects.toString(mServiceConnectors.valueAt(i)))
-                        .append('\n');
-            }
+            mAssociationRequestsProcessor.dump(fout);
 
             fout.append("Device Listener Services State:").append('\n');
             for (int i = 0, size =  mCompanionDevicePresenceController.mBoundServices.size();
@@ -762,7 +575,24 @@
         }
     }
 
-    private static int getCallingUserId() {
+    void checkCallerIsSystemOr(String pkg) throws RemoteException {
+        checkCallerIsSystemOr(pkg, getCallingUserId());
+    }
+
+    private void checkCallerIsSystemOr(String pkg, int userId) throws RemoteException {
+        if (isCallerSystem()) {
+            return;
+        }
+
+        checkArgument(getCallingUserId() == userId,
+                "Must be called by either same user or system");
+        int callingUid = Binder.getCallingUid();
+        if (mAppOpsManager.checkPackage(callingUid, pkg) != AppOpsManager.MODE_ALLOWED) {
+            throw new SecurityException(pkg + " doesn't belong to uid " + callingUid);
+        }
+    }
+
+    static int getCallingUserId() {
         return UserHandle.getUserId(Binder.getCallingUid());
     }
 
@@ -770,6 +600,23 @@
         return Binder.getCallingUid() == Process.SYSTEM_UID;
     }
 
+    void checkUsesFeature(String pkg, int userId) {
+        if (isCallerSystem()) {
+            // Drop the requirement for calls from system process
+            return;
+        }
+
+        FeatureInfo[] reqFeatures = getPackageInfo(pkg, userId).reqFeatures;
+        String requiredFeature = PackageManager.FEATURE_COMPANION_DEVICE_SETUP;
+        int numFeatures = ArrayUtils.size(reqFeatures);
+        for (int i = 0; i < numFeatures; i++) {
+            if (requiredFeature.equals(reqFeatures[i].name)) return;
+        }
+        throw new IllegalStateException("Must declare uses-feature "
+                + requiredFeature
+                + " in manifest to use this API");
+    }
+
     void createAssociationInternal(
             int userId, String deviceMacAddress, String packageName, String deviceProfile) {
         final Association association = new Association(
@@ -950,72 +797,6 @@
         }
     }
 
-    private Set<String> getSameOemPackageCerts(
-            String packageName, String[] oemPackages, String[] sameOemCerts) {
-        Set<String> sameOemPackageCerts = new HashSet<>();
-
-        // Assume OEM may enter same package name in the parallel string array with
-        // multiple APK certs corresponding to it
-        for (int i = 0; i < oemPackages.length; i++) {
-            if (oemPackages[i].equals(packageName)) {
-                sameOemPackageCerts.add(sameOemCerts[i].replaceAll(":", ""));
-            }
-        }
-
-        return sameOemPackageCerts;
-    }
-
-    boolean mayAssociateWithoutPrompt(String packageName, int userId) {
-        String[] sameOemPackages = getContext()
-                .getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
-        if (!ArrayUtils.contains(sameOemPackages, packageName)) {
-            Slog.w(LOG_TAG, packageName
-                    + " can not silently create associations due to no package found."
-                    + " Packages from OEM: " + Arrays.toString(sameOemPackages)
-            );
-            return false;
-        }
-
-        // Throttle frequent associations
-        long now = System.currentTimeMillis();
-        Set<Association> recentAssociations = filter(
-                getAllAssociations(userId, packageName),
-                a -> now - a.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS);
-
-        if (recentAssociations.size() >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
-            Slog.w(LOG_TAG, "Too many associations. " + packageName
-                    + " already associated " + recentAssociations.size()
-                    + " devices within the last " + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS
-                    + "ms: " + recentAssociations);
-            return false;
-        }
-        String[] sameOemCerts = getContext()
-                .getResources()
-                .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
-
-        Signature[] signatures = mPackageManagerInternal
-                .getPackage(packageName).getSigningDetails().getSignatures();
-        String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
-
-        Set<String> sameOemPackageCerts =
-                getSameOemPackageCerts(packageName, sameOemPackages, sameOemCerts);
-
-        for (String cert : apkCerts) {
-            if (sameOemPackageCerts.contains(cert)) {
-                return true;
-            }
-        }
-
-        Slog.w(LOG_TAG, packageName
-                + " can not silently create associations. " + packageName
-                + " has SHA256 certs from APK: " + Arrays.toString(apkCerts)
-                + " and from OEM: " + Arrays.toString(sameOemCerts)
-        );
-
-        return false;
-    }
-
     private static <T> boolean containsEither(T[] array, T a, T b) {
         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
     }
@@ -1120,7 +901,7 @@
         }
     }
 
-    private Set<Association> getAllAssociations(int userId, @Nullable String packageFilter) {
+    Set<Association> getAllAssociations(int userId, @Nullable String packageFilter) {
         return filter(
                 getAllAssociations(userId),
                 // Null filter == get all associations
@@ -1444,25 +1225,6 @@
         return result;
     }
 
-    @NonNull
-    private AndroidFuture<String> getDeviceProfilePermissionDescription(
-            @Nullable String deviceProfile) {
-        if (deviceProfile == null) {
-            return AndroidFuture.completedFuture(null);
-        }
-
-        final AndroidFuture<String> result = new AndroidFuture<>();
-        mPermissionControllerManager.getPrivilegesDescriptionStringForProfile(
-                deviceProfile, FgThread.getExecutor(), desc -> {
-                        try {
-                            result.complete(String.valueOf(desc));
-                        } catch (Exception e) {
-                            result.completeExceptionally(e);
-                        }
-                });
-        return result;
-    }
-
     static int getFirstAssociationIdForUser(@UserIdInt int userId) {
         // We want the IDs to start from 1, not 0.
         return userId * ASSOCIATIONS_IDS_PER_USER_RANGE + 1;