Merge "[Status Bar Refactor] Move PhoneStatusBarTransitions to the dagger component and out of an unrelated notification class."
diff --git a/core/api/current.txt b/core/api/current.txt
index ee44198..08adbcb 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18528,6 +18528,7 @@
     method @NonNull public android.hardware.camera2.CameraExtensionCharacteristics getCameraExtensionCharacteristics(@NonNull String) throws android.hardware.camera2.CameraAccessException;
     method @NonNull public String[] getCameraIdList() throws android.hardware.camera2.CameraAccessException;
     method @NonNull public java.util.Set<java.util.Set<java.lang.String>> getConcurrentCameraIds() throws android.hardware.camera2.CameraAccessException;
+    method public int getTorchStrengthLevel(@NonNull String) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public boolean isConcurrentSessionConfigurationSupported(@NonNull java.util.Map<java.lang.String,android.hardware.camera2.params.SessionConfiguration>) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull android.hardware.camera2.CameraDevice.StateCallback, @Nullable android.os.Handler) throws android.hardware.camera2.CameraAccessException;
     method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
@@ -18536,6 +18537,7 @@
     method public void registerTorchCallback(@NonNull android.hardware.camera2.CameraManager.TorchCallback, @Nullable android.os.Handler);
     method public void registerTorchCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraManager.TorchCallback);
     method public void setTorchMode(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
+    method public void turnOnTorchWithStrengthLevel(@NonNull String, int) throws android.hardware.camera2.CameraAccessException;
     method public void unregisterAvailabilityCallback(@NonNull android.hardware.camera2.CameraManager.AvailabilityCallback);
     method public void unregisterTorchCallback(@NonNull android.hardware.camera2.CameraManager.TorchCallback);
   }
@@ -18553,6 +18555,7 @@
     ctor public CameraManager.TorchCallback();
     method public void onTorchModeChanged(@NonNull String, boolean);
     method public void onTorchModeUnavailable(@NonNull String);
+    method public void onTorchStrengthLevelChanged(@NonNull String, int);
   }
 
   public abstract class CameraMetadata<TKey> {
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index 5750484..77af474 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1352,7 +1352,9 @@
 
         Application app = null;
 
-        String appClass = mApplicationInfo.className;
+        final String myProcessName = Process.myProcessName();
+        String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(
+                myProcessName);
         if (forceDefaultAppClass || (appClass == null)) {
             appClass = "android.app.Application";
         }
diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java
index 6e1f8b5..18a59d8 100644
--- a/core/java/android/companion/AssociationRequest.java
+++ b/core/java/android/companion/AssociationRequest.java
@@ -20,12 +20,15 @@
 
 import static com.android.internal.util.CollectionUtils.emptyIfNull;
 
+import static java.util.Objects.requireNonNull;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.StringDef;
 import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 import android.os.Parcel;
@@ -52,12 +55,11 @@
  * device to be shown instead of a list to choose from
  */
 @DataClass(
+        genConstructor = false,
         genToString = true,
         genEqualsHashCode = true,
         genHiddenGetters = true,
         genParcelable = true,
-        genHiddenConstructor = true,
-        genBuilder = false,
         genConstDefs = false)
 public final class AssociationRequest implements Parcelable {
     /**
@@ -151,40 +153,76 @@
     private final boolean mForceConfirmation;
 
     /**
-     * The app package making the request.
-     *
+     * The app package name of the application the association will belong to.
      * Populated by the system.
-     *
      * @hide
      */
-    private @Nullable String mCallingPackage;
+    private @Nullable String mPackageName;
+
+    /**
+     * The UserId of the user the association will belong to.
+     * Populated by the system.
+     * @hide
+     */
+    private @UserIdInt int mUserId;
 
     /**
      * The user-readable description of the device profile's privileges.
-     *
      * Populated by the system.
-     *
      * @hide
      */
     private @Nullable String mDeviceProfilePrivilegesDescription;
 
     /**
      * The time at which his request was created
-     *
      * @hide
      */
-    private long mCreationTime;
+    private final long mCreationTime;
 
     /**
      * Whether the user-prompt may be skipped once the device is found.
-     *
      * Populated by the system.
-     *
      * @hide
      */
     private boolean mSkipPrompt;
 
     /**
+     * Creates a new AssociationRequest.
+     *
+     * @param singleDevice
+     *   Whether only a single device should match the provided filter.
+     *
+     *   When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
+     *   address, bonded devices are also searched among. This allows to obtain the necessary app
+     *   privileges even if the device is already paired.
+     * @param deviceFilters
+     *   If set, only devices matching either of the given filters will be shown to the user
+     * @param deviceProfile
+     *   Profile of the device.
+     * @param displayName
+     *   The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
+     *   "self-managed" association.
+     * @param selfManaged
+     *   Whether the association is to be managed by the companion application.
+     */
+    private AssociationRequest(
+            boolean singleDevice,
+            @NonNull List<DeviceFilter<?>> deviceFilters,
+            @Nullable @DeviceProfile String deviceProfile,
+            @Nullable CharSequence displayName,
+            boolean selfManaged,
+            boolean forceConfirmation) {
+        mSingleDevice = singleDevice;
+        mDeviceFilters = requireNonNull(deviceFilters);
+        mDeviceProfile = deviceProfile;
+        mDisplayName = displayName;
+        mSelfManaged = selfManaged;
+        mForceConfirmation = forceConfirmation;
+
+        mCreationTime = System.currentTimeMillis();
+    }
+
+    /**
      * @return profile of the companion device.
      */
     public @Nullable @DeviceProfile String getDeviceProfile() {
@@ -237,8 +275,13 @@
     }
 
     /** @hide */
-    public void setCallingPackage(@NonNull String pkg) {
-        mCallingPackage = pkg;
+    public void setPackageName(@NonNull String packageName) {
+        mPackageName = packageName;
+    }
+
+    /** @hide */
+    public void setUserId(@UserIdInt int userId) {
+        mUserId = userId;
     }
 
     /** @hide */
@@ -248,7 +291,7 @@
 
     /** @hide */
     public void setSkipPrompt(boolean value) {
-        mSkipPrompt = true;
+        mSkipPrompt = value;
     }
 
     /** @hide */
@@ -258,10 +301,6 @@
         return mDeviceFilters;
     }
 
-    private void onConstructed() {
-        mCreationTime = System.currentTimeMillis();
-    }
-
     /**
      * A builder for {@link AssociationRequest}
      */
@@ -325,7 +364,7 @@
         @NonNull
         public Builder setDisplayName(@NonNull CharSequence displayName) {
             checkNotUsed();
-            mDisplayName = Objects.requireNonNull(displayName);
+            mDisplayName = requireNonNull(displayName);
             return this;
         }
 
@@ -372,15 +411,13 @@
                         + "provide the display name of the device");
             }
             return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters),
-                    mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation,
-                    null, null, -1L, false);
+                    mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation);
         }
     }
 
 
 
 
-
     // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
@@ -395,88 +432,29 @@
 
 
     /**
-     * Creates a new AssociationRequest.
-     *
-     * @param singleDevice
-     *   Whether only a single device should match the provided filter.
-     *
-     *   When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
-     *   address, bonded devices are also searched among. This allows to obtain the necessary app
-     *   privileges even if the device is already paired.
-     * @param deviceFilters
-     *   If set, only devices matching either of the given filters will be shown to the user
-     * @param deviceProfile
-     *   Profile of the device.
-     * @param displayName
-     *   The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
-     *   "self-managed" association.
-     * @param selfManaged
-     *   Whether the association is to be managed by the companion application.
-     * @param forceConfirmation
-     *   Indicates that the application would prefer the CompanionDeviceManager to collect an explicit
-     *   confirmation from the user before creating an association, even if such confirmation is not
-     *   required.
-     * @param callingPackage
-     *   The app package making the request.
-     *
-     *   Populated by the system.
-     * @param deviceProfilePrivilegesDescription
-     *   The user-readable description of the device profile's privileges.
-     *
-     *   Populated by the system.
-     * @param creationTime
-     *   The time at which his request was created
-     * @param skipPrompt
-     *   Whether the user-prompt may be skipped once the device is found.
-     *
-     *   Populated by the system.
-     * @hide
-     */
-    @DataClass.Generated.Member
-    public AssociationRequest(
-            boolean singleDevice,
-            @NonNull List<DeviceFilter<?>> deviceFilters,
-            @Nullable @DeviceProfile String deviceProfile,
-            @Nullable CharSequence displayName,
-            boolean selfManaged,
-            boolean forceConfirmation,
-            @Nullable String callingPackage,
-            @Nullable String deviceProfilePrivilegesDescription,
-            long creationTime,
-            boolean skipPrompt) {
-        this.mSingleDevice = singleDevice;
-        this.mDeviceFilters = deviceFilters;
-        com.android.internal.util.AnnotationValidations.validate(
-                NonNull.class, null, mDeviceFilters);
-        this.mDeviceProfile = deviceProfile;
-        com.android.internal.util.AnnotationValidations.validate(
-                DeviceProfile.class, null, mDeviceProfile);
-        this.mDisplayName = displayName;
-        this.mSelfManaged = selfManaged;
-        this.mForceConfirmation = forceConfirmation;
-        this.mCallingPackage = callingPackage;
-        this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
-        this.mCreationTime = creationTime;
-        this.mSkipPrompt = skipPrompt;
-
-        onConstructed();
-    }
-
-    /**
-     * The app package making the request.
-     *
+     * The app package name of the application the association will belong to.
      * Populated by the system.
      *
      * @hide
      */
     @DataClass.Generated.Member
-    public @Nullable String getCallingPackage() {
-        return mCallingPackage;
+    public @Nullable String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * The UserId of the user the association will belong to.
+     * Populated by the system.
+     *
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public @UserIdInt int getUserId() {
+        return mUserId;
     }
 
     /**
      * The user-readable description of the device profile's privileges.
-     *
      * Populated by the system.
      *
      * @hide
@@ -498,7 +476,6 @@
 
     /**
      * Whether the user-prompt may be skipped once the device is found.
-     *
      * Populated by the system.
      *
      * @hide
@@ -521,7 +498,8 @@
                 "displayName = " + mDisplayName + ", " +
                 "selfManaged = " + mSelfManaged + ", " +
                 "forceConfirmation = " + mForceConfirmation + ", " +
-                "callingPackage = " + mCallingPackage + ", " +
+                "packageName = " + mPackageName + ", " +
+                "userId = " + mUserId + ", " +
                 "deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription + ", " +
                 "creationTime = " + mCreationTime + ", " +
                 "skipPrompt = " + mSkipPrompt +
@@ -547,7 +525,8 @@
                 && Objects.equals(mDisplayName, that.mDisplayName)
                 && mSelfManaged == that.mSelfManaged
                 && mForceConfirmation == that.mForceConfirmation
-                && Objects.equals(mCallingPackage, that.mCallingPackage)
+                && Objects.equals(mPackageName, that.mPackageName)
+                && mUserId == that.mUserId
                 && Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription)
                 && mCreationTime == that.mCreationTime
                 && mSkipPrompt == that.mSkipPrompt;
@@ -566,7 +545,8 @@
         _hash = 31 * _hash + Objects.hashCode(mDisplayName);
         _hash = 31 * _hash + Boolean.hashCode(mSelfManaged);
         _hash = 31 * _hash + Boolean.hashCode(mForceConfirmation);
-        _hash = 31 * _hash + Objects.hashCode(mCallingPackage);
+        _hash = 31 * _hash + Objects.hashCode(mPackageName);
+        _hash = 31 * _hash + mUserId;
         _hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription);
         _hash = 31 * _hash + Long.hashCode(mCreationTime);
         _hash = 31 * _hash + Boolean.hashCode(mSkipPrompt);
@@ -583,16 +563,17 @@
         if (mSingleDevice) flg |= 0x1;
         if (mSelfManaged) flg |= 0x10;
         if (mForceConfirmation) flg |= 0x20;
-        if (mSkipPrompt) flg |= 0x200;
+        if (mSkipPrompt) flg |= 0x400;
         if (mDeviceProfile != null) flg |= 0x4;
         if (mDisplayName != null) flg |= 0x8;
-        if (mCallingPackage != null) flg |= 0x40;
-        if (mDeviceProfilePrivilegesDescription != null) flg |= 0x80;
+        if (mPackageName != null) flg |= 0x40;
+        if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100;
         dest.writeInt(flg);
         dest.writeParcelableList(mDeviceFilters, flags);
         if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
         if (mDisplayName != null) dest.writeCharSequence(mDisplayName);
-        if (mCallingPackage != null) dest.writeString(mCallingPackage);
+        if (mPackageName != null) dest.writeString(mPackageName);
+        dest.writeInt(mUserId);
         if (mDeviceProfilePrivilegesDescription != null) dest.writeString(mDeviceProfilePrivilegesDescription);
         dest.writeLong(mCreationTime);
     }
@@ -612,13 +593,14 @@
         boolean singleDevice = (flg & 0x1) != 0;
         boolean selfManaged = (flg & 0x10) != 0;
         boolean forceConfirmation = (flg & 0x20) != 0;
-        boolean skipPrompt = (flg & 0x200) != 0;
+        boolean skipPrompt = (flg & 0x400) != 0;
         List<DeviceFilter<?>> deviceFilters = new ArrayList<>();
         in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader());
         String deviceProfile = (flg & 0x4) == 0 ? null : in.readString();
         CharSequence displayName = (flg & 0x8) == 0 ? null : (CharSequence) in.readCharSequence();
-        String callingPackage = (flg & 0x40) == 0 ? null : in.readString();
-        String deviceProfilePrivilegesDescription = (flg & 0x80) == 0 ? null : in.readString();
+        String packageName = (flg & 0x40) == 0 ? null : in.readString();
+        int userId = in.readInt();
+        String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString();
         long creationTime = in.readLong();
 
         this.mSingleDevice = singleDevice;
@@ -631,12 +613,15 @@
         this.mDisplayName = displayName;
         this.mSelfManaged = selfManaged;
         this.mForceConfirmation = forceConfirmation;
-        this.mCallingPackage = callingPackage;
+        this.mPackageName = packageName;
+        this.mUserId = userId;
+        com.android.internal.util.AnnotationValidations.validate(
+                UserIdInt.class, null, mUserId);
         this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
         this.mCreationTime = creationTime;
         this.mSkipPrompt = skipPrompt;
 
-        onConstructed();
+        // onConstructed(); // You can define this method to get a callback
     }
 
     @DataClass.Generated.Member
@@ -654,10 +639,10 @@
     };
 
     @DataClass.Generated(
-            time = 1638368698639L,
+            time = 1638962248060L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/companion/AssociationRequest.java",
-            inputSignatures = "public static final  java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final  boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final  boolean mSelfManaged\nprivate final  boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mCallingPackage\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate  long mCreationTime\nprivate  boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic  boolean isSingleDevice()\npublic  void setCallingPackage(java.lang.String)\npublic  void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic  void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nprivate  void onConstructed()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate  boolean mSelfManaged\nprivate  boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genHiddenConstructor=true, genBuilder=false, genConstDefs=false)")
+            inputSignatures = "public static final  java.lang.String DEVICE_PROFILE_WATCH\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_APP_STREAMING\npublic static final @android.annotation.RequiresPermission @android.annotation.SystemApi java.lang.String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION\nprivate final  boolean mSingleDevice\nprivate final @com.android.internal.util.DataClass.PluralOf(\"deviceFilter\") @android.annotation.NonNull java.util.List<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate final @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String mDeviceProfile\nprivate final @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate final  boolean mSelfManaged\nprivate final  boolean mForceConfirmation\nprivate @android.annotation.Nullable java.lang.String mPackageName\nprivate @android.annotation.UserIdInt int mUserId\nprivate @android.annotation.Nullable java.lang.String mDeviceProfilePrivilegesDescription\nprivate final  long mCreationTime\nprivate  boolean mSkipPrompt\npublic @android.annotation.Nullable @android.companion.AssociationRequest.DeviceProfile java.lang.String getDeviceProfile()\npublic @android.annotation.Nullable java.lang.CharSequence getDisplayName()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isSelfManaged()\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission boolean isForceConfirmation()\npublic  boolean isSingleDevice()\npublic  void setPackageName(java.lang.String)\npublic  void setUserId(int)\npublic  void setDeviceProfilePrivilegesDescription(java.lang.String)\npublic  void setSkipPrompt(boolean)\npublic @android.annotation.NonNull @android.compat.annotation.UnsupportedAppUsage java.util.List<android.companion.DeviceFilter<?>> getDeviceFilters()\nclass AssociationRequest extends java.lang.Object implements [android.os.Parcelable]\nprivate  boolean mSingleDevice\nprivate @android.annotation.Nullable java.util.ArrayList<android.companion.DeviceFilter<?>> mDeviceFilters\nprivate @android.annotation.Nullable java.lang.String mDeviceProfile\nprivate @android.annotation.Nullable java.lang.CharSequence mDisplayName\nprivate  boolean mSelfManaged\nprivate  boolean mForceConfirmation\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setSingleDevice(boolean)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter<?>)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDeviceProfile(java.lang.String)\npublic @android.annotation.NonNull android.companion.AssociationRequest.Builder setDisplayName(java.lang.CharSequence)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setSelfManaged(boolean)\npublic @android.annotation.SystemApi @android.annotation.RequiresPermission @android.annotation.NonNull android.companion.AssociationRequest.Builder setForceConfirmation(boolean)\npublic @android.annotation.NonNull @java.lang.Override android.companion.AssociationRequest build()\nclass Builder extends android.provider.OneTimeUseBuilder<android.companion.AssociationRequest> implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genEqualsHashCode=true, genHiddenGetters=true, genParcelable=true, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 2998f76..76b4e5c 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -33,6 +33,7 @@
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
+import android.util.ArrayMap;
 import android.util.Printer;
 import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
@@ -1533,6 +1534,15 @@
 
     private int mHiddenApiPolicy = HIDDEN_API_ENFORCEMENT_DEFAULT;
 
+    /**
+     * A map from a process name to an (custom) application class name in this package, derived
+     * from the <processes> tag in the app's manifest. This map may not contain all the process
+     * names. Processses not in this map will use the default app class name,
+     * which is {@link #className}, or the default class {@link android.app.Application}.
+     */
+    @Nullable
+    private ArrayMap<String, String> mAppClassNamesByProcess;
+
     public void dump(Printer pw, String prefix) {
         dump(pw, prefix, DUMP_FLAG_ALL);
     }
@@ -1540,8 +1550,14 @@
     /** @hide */
     public void dump(Printer pw, String prefix, int dumpFlags) {
         super.dumpFront(pw, prefix);
-        if ((dumpFlags & DUMP_FLAG_DETAILS) != 0 && className != null) {
-            pw.println(prefix + "className=" + className);
+        if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+            if (className != null) {
+                pw.println(prefix + "className=" + className);
+            }
+            for (int i = 0; i < ArrayUtils.size(mAppClassNamesByProcess); i++) {
+                pw.println(prefix + "  process=" + mAppClassNamesByProcess.keyAt(i)
+                        + " className=" + mAppClassNamesByProcess.valueAt(i));
+            }
         }
         if (permission != null) {
             pw.println(prefix + "permission=" + permission);
@@ -1967,6 +1983,16 @@
         dest.writeInt(nativeHeapZeroInitialized);
         sForBoolean.parcel(requestRawExternalStorageAccess, dest, parcelableFlags);
         dest.writeLong(createTimestamp);
+        if (mAppClassNamesByProcess == null) {
+            dest.writeInt(0);
+        } else {
+            final int size = mAppClassNamesByProcess.size();
+            dest.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                dest.writeString(mAppClassNamesByProcess.keyAt(i));
+                dest.writeString(mAppClassNamesByProcess.valueAt(i));
+            }
+        }
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<ApplicationInfo> CREATOR
@@ -2055,6 +2081,13 @@
         nativeHeapZeroInitialized = source.readInt();
         requestRawExternalStorageAccess = sForBoolean.unparcel(source);
         createTimestamp = source.readLong();
+        final int allClassesSize = source.readInt();
+        if (allClassesSize > 0) {
+            mAppClassNamesByProcess = new ArrayMap<>(allClassesSize);
+            for (int i = 0; i < allClassesSize; i++) {
+                mAppClassNamesByProcess.put(source.readString(), source.readString());
+            }
+        }
     }
 
     /**
@@ -2538,6 +2571,19 @@
         requestRawExternalStorageAccess = value;
     }
 
+    /**
+     * Replaces {@link #mAppClassNamesByProcess}. This takes over the ownership of the passed map.
+     * Do not modify the argument at the callsite.
+     * {@hide}
+     */
+    public void setAppClassNamesByProcess(@Nullable ArrayMap<String, String> value) {
+        if (ArrayUtils.size(value) == 0) {
+            mAppClassNamesByProcess = null;
+        } else {
+            mAppClassNamesByProcess = value;
+        }
+    }
+
     /** {@hide} */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public String getCodePath() { return scanSourceDir; }
@@ -2568,4 +2614,21 @@
     public int getNativeHeapZeroInitialized() {
         return nativeHeapZeroInitialized;
     }
+
+    /**
+     * Return the application class name defined in the manifest. The class name set in the
+     * <processes> tag for this process, then return it. Otherwise it'll return the class
+     * name set in the <application> tag. If neither is set, it'll return null.
+     * @hide
+     */
+    @Nullable
+    public String getCustomApplicationClassNameForProcess(String processName) {
+        if (mAppClassNamesByProcess != null) {
+            String byProcess = mAppClassNamesByProcess.get(processName);
+            if (byProcess != null) {
+                return byProcess;
+            }
+        }
+        return className;
+    }
 }
diff --git a/core/java/android/content/pm/parsing/ParsingPackage.java b/core/java/android/content/pm/parsing/ParsingPackage.java
index 2fa5df7..fc9f1a5 100644
--- a/core/java/android/content/pm/parsing/ParsingPackage.java
+++ b/core/java/android/content/pm/parsing/ParsingPackage.java
@@ -118,6 +118,7 @@
 
     ParsingPackage addQueriesProvider(String authority);
 
+    /** Sets a process name -> {@link ParsedProcess} map coming from the <processes> tag. */
     ParsingPackage setProcesses(@NonNull Map<String, ParsedProcess> processes);
 
     ParsingPackage asSplit(
diff --git a/core/java/android/content/pm/parsing/ParsingPackageImpl.java b/core/java/android/content/pm/parsing/ParsingPackageImpl.java
index f03ab6a..424f477 100644
--- a/core/java/android/content/pm/parsing/ParsingPackageImpl.java
+++ b/core/java/android/content/pm/parsing/ParsingPackageImpl.java
@@ -62,6 +62,7 @@
 import android.os.Parcelable;
 import android.os.storage.StorageManager;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -297,6 +298,9 @@
 //    @DataClass.ParcelWith(ParsingUtils.StringPairListParceler.class)
     private List<Pair<String, ParsedIntentInfo>> preferredActivityFilters = emptyList();
 
+    /**
+     * Map from a process name to a {@link ParsedProcess}.
+     */
     @NonNull
     private Map<String, ParsedProcess> processes = emptyMap();
 
@@ -1131,10 +1135,46 @@
         appInfo.setSplitCodePaths(splitCodePaths);
         appInfo.setSplitResourcePaths(splitCodePaths);
         appInfo.setVersionCode(mLongVersionCode);
+        appInfo.setAppClassNamesByProcess(buildAppClassNamesByProcess());
 
         return appInfo;
     }
 
+    /**
+     * Create a map from a process name to the custom application class for this process,
+     * which comes from <processes><process android:name="xxx">.
+     *
+     * The original information is stored in {@link #processes}, but it's stored in
+     * a form of: [process name] -[1:N]-> [package name] -[1:N]-> [class name].
+     * We scan it and collect the process names and their app class names, only for this package.
+     *
+     * The resulting map only contains processes with a custom application class set.
+     */
+    @Nullable
+    private ArrayMap<String, String> buildAppClassNamesByProcess() {
+        if (processes == null) {
+            return null;
+        }
+        final ArrayMap<String, String> ret = new ArrayMap<>(4);
+        for (String processName : processes.keySet()) {
+            final ParsedProcess process = processes.get(processName);
+            final ArrayMap<String, String> appClassesByPackage =
+                    process.getAppClassNamesByPackage();
+
+            for (int i = 0; i < appClassesByPackage.size(); i++) {
+                final String packageName = appClassesByPackage.keyAt(i);
+
+                if (this.packageName.equals(packageName)) {
+                    final String appClassName = appClassesByPackage.valueAt(i);
+                    if (!TextUtils.isEmpty(appClassName)) {
+                        ret.put(processName, appClassName);
+                    }
+                }
+            }
+        }
+        return ret;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcess.java b/core/java/android/content/pm/parsing/component/ParsedProcess.java
index c2d5163..27a540d 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcess.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcess.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.pm.ApplicationInfo;
 import android.os.Parcelable;
+import android.util.ArrayMap;
 
 import java.util.Set;
 
@@ -37,6 +38,15 @@
     @NonNull
     String getName();
 
+    /**
+     * The app class names in this (potentially shared) process, from a package name to
+     * the application class name.
+     * It's a map, because in shared processes, different packages can have different application
+     * classes.
+     */
+    @NonNull
+    ArrayMap<String, String> getAppClassNamesByPackage();
+
     @ApplicationInfo.NativeHeapZeroInitialized
     int getNativeHeapZeroInitialized();
 }
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java b/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
index 3fd60eb..d404ecfd 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java
@@ -22,6 +22,7 @@
 import android.content.pm.ApplicationInfo;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -39,6 +40,11 @@
 
     @NonNull
     private String name;
+
+    /** @see ParsedProcess#getAppClassNamesByPackage() */
+    @NonNull
+    private ArrayMap<String, String> appClassNamesByPackage = ArrayMap.EMPTY;
+
     @NonNull
     @DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedStringSet.class)
     private Set<String> deniedPermissions = emptySet();
@@ -55,6 +61,8 @@
 
     public ParsedProcessImpl(@NonNull ParsedProcess other) {
         name = other.getName();
+        appClassNamesByPackage = (other.getAppClassNamesByPackage().size() == 0)
+                ? ArrayMap.EMPTY : new ArrayMap<>(other.getAppClassNamesByPackage());
         deniedPermissions = new ArraySet<>(other.getDeniedPermissions());
         gwpAsanMode = other.getGwpAsanMode();
         memtagMode = other.getMemtagMode();
@@ -66,6 +74,21 @@
         gwpAsanMode = other.getGwpAsanMode();
         memtagMode = other.getMemtagMode();
         nativeHeapZeroInitialized = other.getNativeHeapZeroInitialized();
+
+        final ArrayMap<String, String> oacn = other.getAppClassNamesByPackage();
+        for (int i = 0; i < oacn.size(); i++) {
+            appClassNamesByPackage.put(oacn.keyAt(i), oacn.valueAt(i));
+        }
+    }
+
+    /**
+     * Sets a custom application name used in this process for a given package.
+     */
+    public void putAppClassNameForPackage(String packageName, String className) {
+        if (appClassNamesByPackage.size() == 0) {
+            appClassNamesByPackage = new ArrayMap<>(4);
+        }
+        appClassNamesByPackage.put(packageName, className);
     }
 
 
@@ -83,9 +106,14 @@
     //@formatter:off
 
 
+    /**
+     * Creates a new ParsedProcessImpl.
+     *
+     */
     @DataClass.Generated.Member
     public ParsedProcessImpl(
             @NonNull String name,
+            @NonNull ArrayMap<String,String> appClassNamesByPackage,
             @NonNull Set<String> deniedPermissions,
             @ApplicationInfo.GwpAsanMode int gwpAsanMode,
             @ApplicationInfo.MemtagMode int memtagMode,
@@ -93,6 +121,9 @@
         this.name = name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, name);
+        this.appClassNamesByPackage = appClassNamesByPackage;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
         this.deniedPermissions = deniedPermissions;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, deniedPermissions);
@@ -114,6 +145,14 @@
         return name;
     }
 
+    /**
+     * @see ParsedProcess#getAppClassNamesByPackage()
+     */
+    @DataClass.Generated.Member
+    public @NonNull ArrayMap<String,String> getAppClassNamesByPackage() {
+        return appClassNamesByPackage;
+    }
+
     @DataClass.Generated.Member
     public @NonNull Set<String> getDeniedPermissions() {
         return deniedPermissions;
@@ -142,6 +181,17 @@
         return this;
     }
 
+    /**
+     * @see ParsedProcess#getAppClassNamesByPackage()
+     */
+    @DataClass.Generated.Member
+    public @NonNull ParsedProcessImpl setAppClassNamesByPackage(@NonNull ArrayMap<String,String> value) {
+        appClassNamesByPackage = value;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
+        return this;
+    }
+
     @DataClass.Generated.Member
     public @NonNull ParsedProcessImpl setDeniedPermissions(@NonNull Set<String> value) {
         deniedPermissions = value;
@@ -192,6 +242,7 @@
         // void parcelFieldName(Parcel dest, int flags) { ... }
 
         dest.writeString(name);
+        dest.writeMap(appClassNamesByPackage);
         sParcellingForDeniedPermissions.parcel(deniedPermissions, dest, flags);
         dest.writeInt(gwpAsanMode);
         dest.writeInt(memtagMode);
@@ -210,6 +261,8 @@
         // static FieldType unparcelFieldName(Parcel in) { ... }
 
         String _name = in.readString();
+        ArrayMap<String,String> _appClassNamesByPackage = new ArrayMap();
+        in.readMap(_appClassNamesByPackage, String.class.getClassLoader());
         Set<String> _deniedPermissions = sParcellingForDeniedPermissions.unparcel(in);
         int _gwpAsanMode = in.readInt();
         int _memtagMode = in.readInt();
@@ -218,6 +271,9 @@
         this.name = _name;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, name);
+        this.appClassNamesByPackage = _appClassNamesByPackage;
+        com.android.internal.util.AnnotationValidations.validate(
+                NonNull.class, null, appClassNamesByPackage);
         this.deniedPermissions = _deniedPermissions;
         com.android.internal.util.AnnotationValidations.validate(
                 NonNull.class, null, deniedPermissions);
@@ -249,10 +305,10 @@
     };
 
     @DataClass.Generated(
-            time = 1627605368434L,
+            time = 1639076603310L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/component/ParsedProcessImpl.java",
-            inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic  void addStateFrom(android.content.pm.parsing.component.ParsedProcess)\nclass ParsedProcessImpl extends java.lang.Object implements [android.content.pm.parsing.component.ParsedProcess]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
+            inputSignatures = "private @android.annotation.NonNull java.lang.String name\nprivate @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.String> appClassNamesByPackage\nprivate @android.annotation.NonNull @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedStringSet.class) java.util.Set<java.lang.String> deniedPermissions\nprivate @android.content.pm.ApplicationInfo.GwpAsanMode int gwpAsanMode\nprivate @android.content.pm.ApplicationInfo.MemtagMode int memtagMode\nprivate @android.content.pm.ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized\npublic  void addStateFrom(android.content.pm.parsing.component.ParsedProcess)\npublic  void putAppClassNameForPackage(java.lang.String,java.lang.String)\nclass ParsedProcessImpl extends java.lang.Object implements [android.content.pm.parsing.component.ParsedProcess]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genParcelable=true, genAidl=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java b/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
index cf83586..5e4cf66 100644
--- a/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
+++ b/core/java/android/content/pm/parsing/component/ParsedProcessUtils.java
@@ -97,7 +97,12 @@
                 return input.error(processNameResult);
             }
 
+            String packageName = pkg.getPackageName();
+            String className = ParsingUtils.buildClassName(packageName,
+                    sa.getNonConfigurationString(R.styleable.AndroidManifestProcess_name, 0));
+
             proc.setName(processNameResult.getResult());
+            proc.putAppClassNameForPackage(packageName, className);
             proc.setGwpAsanMode(sa.getInt(R.styleable.AndroidManifestProcess_gwpAsanMode, -1));
             proc.setMemtagMode(sa.getInt(R.styleable.AndroidManifestProcess_memtagMode, -1));
             if (sa.hasValue(R.styleable.AndroidManifestProcess_nativeHeapZeroInitialized)) {
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 93f1d61..c12e819 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -1036,6 +1036,95 @@
     }
 
     /**
+     * Set the brightness level of the flashlight associated with the given cameraId in torch
+     * mode. If the torch is OFF and torchStrength is >= 1, torch will turn ON with the
+     * strength level specified in torchStrength.
+     *
+     * <p>Use
+     * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}
+     * to check whether the camera device supports flash unit strength control or not. If this value
+     * is greater than 1, applications can call this API to control the flashlight brightness level.
+     * </p>
+     *
+     * <p>If {@link #turnOnTorchWithStrengthLevel} is called to change the brightness level of the
+     * flash unit {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will be invoked.
+     * If the new desired strength level is same as previously set level, then this callback will
+     * not be invoked.
+     * If the torch is OFF and {@link #turnOnTorchWithStrengthLevel} is called with level >= 1,
+     * the torch will be turned ON with that brightness level. In this case
+     * {@link CameraManager.TorchCallback#onTorchModeChanged} will also be invoked.
+     * </p>
+     *
+     * <p>When the torch is turned OFF via {@link #setTorchMode}, the flashlight brightness level
+     * will reset to default value
+     * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+     * In this case the {@link CameraManager.TorchCallback#onTorchStrengthLevelChanged} will not be
+     * invoked.
+     * </p>
+     *
+     * <p>If torch is enabled via {@link #setTorchMode} after calling
+     * {@link #turnOnTorchWithStrengthLevel} with level N then the flash unit will have the
+     * brightness level N.
+     * Since multiple applications are free to call {@link #setTorchMode}, when the latest
+     * application that turned ON the torch mode exits, the torch mode will be turned OFF
+     * and in this case the brightness level will reset to default level.
+     * </p>
+     *
+     * @param cameraId
+     *             The unique identifier of the camera device that the flash unit belongs to.
+     * @param torchStrength
+     *             The desired brightness level to be set for the flash unit in the range 1 to
+     *             {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}.
+     *
+     * @throws CameraAccessException if it failed to access the flash unit.
+     *             {@link CameraAccessException#CAMERA_IN_USE} will be thrown if the camera device
+     *             is in use. {@link CameraAccessException#MAX_CAMERAS_IN_USE} will be thrown if
+     *             other camera resources needed to turn on the torch mode are in use.
+     *             {@link CameraAccessException#CAMERA_DISCONNECTED} will be thrown if camera
+     *             service is not available.
+     * @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently
+     *              or previously available camera device, the camera device doesn't have a
+     *              flash unit or if torchStrength is not within the range i.e. is greater than
+     *              the maximum level
+     *              {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_MAXIMUM_LEVEL}
+     *              or <= 0.
+     *
+     */
+    public void turnOnTorchWithStrengthLevel(@NonNull String cameraId, int torchStrength)
+            throws CameraAccessException {
+        if (CameraManagerGlobal.sCameraServiceDisabled) {
+            throw new IllegalArgumentException("No camera available on device");
+        }
+        CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength);
+    }
+
+    /**
+     * Returns the brightness level of the flash unit associated with the cameraId.
+     *
+     * @param cameraId
+     *              The unique identifier of the camera device that the flash unit belongs to.
+     * @return The brightness level of the flash unit associated with cameraId.
+     *         When the torch is turned OFF, the strength level will reset to a default level
+     *         {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}.
+     *         In this case the return value will be
+     *         {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+     *         rather than 0.
+     *
+     * @throws CameraAccessException if it failed to access the flash unit.
+     * @throws IllegalArgumentException if cameraId was null, cameraId doesn't match any currently
+     *              or previously available camera device, or the camera device doesn't have a
+     *              flash unit.
+     *
+     */
+    public int getTorchStrengthLevel(@NonNull String cameraId)
+            throws CameraAccessException {
+        if (CameraManagerGlobal.sCameraServiceDisabled) {
+            throw new IllegalArgumentException("No camera available on device.");
+        }
+        return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId);
+    }
+
+    /**
      * A callback for camera devices becoming available or unavailable to open.
      *
      * <p>Cameras become available when they are no longer in use, or when a new
@@ -1239,6 +1328,24 @@
         public void onTorchModeChanged(@NonNull String cameraId, boolean enabled) {
             // default empty implementation
         }
+
+        /**
+         * A camera's flash unit brightness level has been changed in torch mode via
+         * {@link #turnOnTorchWithStrengthLevel}. When the torch is turned OFF, this
+         * callback will not be triggered even though the torch strength level resets to
+         * default value
+         * {@link android.hardware.camera2.CameraCharacteristics#FLASH_INFO_STRENGTH_DEFAULT_LEVEL}
+         *
+         * <p>The default implementation of this method does nothing.</p>
+         *
+         * @param cameraId The unique identifier of the camera whose flash unit brightness level has
+         * been changed.
+         *
+         * @param newStrengthLevel The brightness level of the flash unit that has been changed to.
+         */
+        public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) {
+            // default empty implementation
+        }
     }
 
     /**
@@ -1642,6 +1749,10 @@
                 public void onTorchStatusChanged(int status, String id) throws RemoteException {
                 }
                 @Override
+                public void onTorchStrengthLevelChanged(String id, int newStrengthLevel)
+                        throws RemoteException {
+                }
+                @Override
                 public void onCameraAccessPrioritiesChanged() {
                 }
                 @Override
@@ -1825,6 +1936,57 @@
             }
         }
 
+        public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength) throws
+                CameraAccessException {
+            synchronized(mLock) {
+
+                if (cameraId == null) {
+                    throw new IllegalArgumentException("cameraId was null");
+                }
+
+                ICameraService cameraService = getCameraService();
+                if (cameraService == null) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                        "Camera service is currently unavailable.");
+                }
+
+                try {
+                    cameraService.turnOnTorchWithStrengthLevel(cameraId, torchStrength,
+                            mTorchClientBinder);
+                } catch(ServiceSpecificException e) {
+                    throwAsPublicException(e);
+                } catch (RemoteException e) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                            "Camera service is currently unavailable.");
+                }
+            }
+        }
+
+        public int getTorchStrengthLevel(String cameraId) throws CameraAccessException {
+            int torchStrength = 0;
+            synchronized(mLock) {
+                if (cameraId == null) {
+                    throw new IllegalArgumentException("cameraId was null");
+                }
+
+                ICameraService cameraService = getCameraService();
+                if (cameraService == null) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                        "Camera service is currently unavailable.");
+                }
+
+                try {
+                    torchStrength = cameraService.getTorchStrengthLevel(cameraId);
+                } catch(ServiceSpecificException e) {
+                    throwAsPublicException(e);
+                } catch (RemoteException e) {
+                    throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
+                            "Camera service is currently unavailable.");
+                }
+            }
+            return torchStrength;
+        }
+
         private void handleRecoverableSetupErrors(ServiceSpecificException e) {
             switch (e.errorCode) {
                 case ICameraService.ERROR_DISCONNECTED:
@@ -1984,6 +2146,18 @@
             }
         }
 
+        private void postSingleTorchStrengthLevelUpdate(final TorchCallback callback,
+                 final Executor executor, final String id, final int newStrengthLevel) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                executor.execute(() -> {
+                    callback.onTorchStrengthLevelChanged(id, newStrengthLevel);
+                });
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
         /**
          * Send the state of all known cameras to the provided listener, to initialize
          * the listener's knowledge of camera state.
@@ -2167,6 +2341,22 @@
             }
         } // onTorchStatusChangedLocked
 
+        private void onTorchStrengthLevelChangedLocked(String cameraId, int newStrengthLevel) {
+            if (DEBUG) {
+
+                Log.v(TAG,
+                        String.format("Camera id %s has torch strength level changed to %d",
+                            cameraId, newStrengthLevel));
+            }
+
+            final int callbackCount = mTorchCallbackMap.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final Executor executor = mTorchCallbackMap.valueAt(i);
+                final TorchCallback callback = mTorchCallbackMap.keyAt(i);
+                postSingleTorchStrengthLevelUpdate(callback, executor, cameraId, newStrengthLevel);
+            }
+        } // onTorchStrengthLevelChanged
+
         /**
          * Register a callback to be notified about camera device availability with the
          * global listener singleton.
@@ -2258,6 +2448,14 @@
         }
 
         @Override
+        public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel)
+                throws RemoteException {
+            synchronized (mLock) {
+                onTorchStrengthLevelChangedLocked(cameraId, newStrengthLevel);
+            }
+        }
+
+        @Override
         public void onCameraAccessPrioritiesChanged() {
             synchronized (mLock) {
                 final int callbackCount = mCallbackMap.size();
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 0a6ef7c..06f347f 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2584,6 +2584,9 @@
     <declare-styleable name="AndroidManifestProcess" parent="AndroidManifestProcesses">
         <!-- Required name of the process that is allowed -->
         <attr name="process" />
+        <!-- custom Application class name. We use call it "name", not "className", to be
+             consistent with the Application tag. -->
+        <attr name="name" />
         <attr name="gwpAsanMode" />
         <attr name="memtagMode" />
         <attr name="nativeHeapZeroInitialized" />
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 81db63e..624940b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -394,6 +394,8 @@
         <!-- Permissions required for Incremental CTS tests -->
         <permission name="com.android.permission.USE_INSTALLER_V2"/>
         <permission name="android.permission.LOADER_USAGE_STATS"/>
+        <!-- Permissions required for Package Verifier tests -->
+        <permission name="android.permission.PACKAGE_VERIFICATION_AGENT" />
         <!-- Permission required to test system only camera devices. -->
         <permission name="android.permission.SYSTEM_CAMERA" />
         <!-- Permission required to test ExplicitHealthCheckServiceImpl. -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index cd635c1..51b7eaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -562,17 +562,10 @@
         overflowBubble(reason, bubbleToRemove);
 
         if (mBubbles.size() == 1) {
-            if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
-                // No more active bubbles but we have stuff in the overflow -- select that view
-                // if we're already expanded or always showing.
-                setShowingOverflow(true);
-                setSelectedBubbleInternal(mOverflow);
-            } else {
-                setExpandedInternal(false);
-                // Don't use setSelectedBubbleInternal because we don't want to trigger an
-                // applyUpdate
-                mSelectedBubble = null;
-            }
+            setExpandedInternal(false);
+            // Don't use setSelectedBubbleInternal because we don't want to trigger an
+            // applyUpdate
+            mSelectedBubble = null;
         }
         if (indexToRemove < mBubbles.size() - 1) {
             // Removing anything but the last bubble means positions will change.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index b65a2e4..8e6c05d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -151,7 +151,13 @@
                 final Rect rightHitRegion = new Rect();
                 final Rect rightDrawRegion = bottomOrRightBounds;
 
-                displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                // If we have existing split regions use those bounds, otherwise split it 50/50
+                if (inSplitScreen) {
+                    leftHitRegion.set(topOrLeftBounds);
+                    rightHitRegion.set(bottomOrRightBounds);
+                } else {
+                    displayRegion.splitVertically(leftHitRegion, rightHitRegion);
+                }
 
                 mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion));
@@ -162,8 +168,13 @@
                 final Rect bottomHitRegion = new Rect();
                 final Rect bottomDrawRegion = bottomOrRightBounds;
 
-                displayRegion.splitHorizontally(
-                        topHitRegion, bottomHitRegion);
+                // If we have existing split regions use those bounds, otherwise split it 50/50
+                if (inSplitScreen) {
+                    topHitRegion.set(topOrLeftBounds);
+                    bottomHitRegion.set(bottomOrRightBounds);
+                } else {
+                    displayRegion.splitHorizontally(topHitRegion, bottomHitRegion);
+                }
 
                 mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion));
                 mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 67f9062..fd3be2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -17,7 +17,9 @@
 package com.android.wm.shell.draganddrop;
 
 import static android.app.StatusBarManager.DISABLE_NONE;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 
 import android.animation.Animator;
@@ -32,11 +34,11 @@
 import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.RemoteException;
 import android.view.DragEvent;
 import android.view.SurfaceControl;
-import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
 import android.widget.LinearLayout;
@@ -73,6 +75,7 @@
     private DropZoneView mDropZoneView2;
 
     private int mDisplayMargin;
+    private int mDividerSize;
     private Insets mInsets = Insets.NONE;
 
     private boolean mIsShowing;
@@ -89,13 +92,15 @@
 
         mDisplayMargin = context.getResources().getDimensionPixelSize(
                 R.dimen.drop_layout_display_margin);
+        mDividerSize = context.getResources().getDimensionPixelSize(
+                R.dimen.split_divider_bar_width);
 
         mDropZoneView1 = new DropZoneView(context);
         mDropZoneView2 = new DropZoneView(context);
-        addView(mDropZoneView1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT));
-        addView(mDropZoneView2, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.MATCH_PARENT));
+        addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT,
+                MATCH_PARENT));
+        addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT,
+                MATCH_PARENT));
         ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1;
         ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1;
         updateContainerMargins();
@@ -165,44 +170,82 @@
         mHasDropped = false;
         mCurrentTarget = null;
 
-        List<ActivityManager.RunningTaskInfo> tasks = null;
-        // Figure out the splashscreen info for the existing task(s).
-        try {
-            tasks = ActivityTaskManager.getService().getTasks(2,
-                            false /* filterOnlyVisibleRecents */,
-                            false /* keepIntentExtra */);
-        } catch (RemoteException e) {
-            // don't show an icon / will just use the defaults
-        }
-        if (tasks != null && !tasks.isEmpty()) {
-            ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
-            Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
-            int bgColor1 = getResizingBackgroundColor(taskInfo1);
-
-            boolean alreadyInSplit = mSplitScreenController != null
-                    && mSplitScreenController.isSplitScreenVisible();
-            if (alreadyInSplit && tasks.size() > 1) {
-                ActivityManager.RunningTaskInfo taskInfo2 = tasks.get(1);
-                Drawable icon2 = mIconProvider.getIcon(taskInfo2.topActivityInfo);
-                int bgColor2 = getResizingBackgroundColor(taskInfo2);
-
-                // figure out which task is on which side
-                int splitPosition1 = mSplitScreenController.getSplitPosition(taskInfo1.taskId);
-                boolean isTask1TopOrLeft = splitPosition1 == SPLIT_POSITION_TOP_OR_LEFT;
-                if (isTask1TopOrLeft) {
-                    mDropZoneView1.setAppInfo(bgColor1, icon1);
-                    mDropZoneView2.setAppInfo(bgColor2, icon2);
-                } else {
-                    mDropZoneView2.setAppInfo(bgColor1, icon1);
-                    mDropZoneView1.setAppInfo(bgColor2, icon2);
-                }
-            } else {
+        boolean alreadyInSplit = mSplitScreenController != null
+                && mSplitScreenController.isSplitScreenVisible();
+        if (!alreadyInSplit) {
+            List<ActivityManager.RunningTaskInfo> tasks = null;
+            // Figure out the splashscreen info for the existing task.
+            try {
+                tasks = ActivityTaskManager.getService().getTasks(1,
+                        false /* filterOnlyVisibleRecents */,
+                        false /* keepIntentExtra */);
+            } catch (RemoteException e) {
+                // don't show an icon / will just use the defaults
+            }
+            if (tasks != null && !tasks.isEmpty()) {
+                ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0);
+                Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
+                int bgColor1 = getResizingBackgroundColor(taskInfo1);
                 mDropZoneView1.setAppInfo(bgColor1, icon1);
                 mDropZoneView2.setAppInfo(bgColor1, icon1);
+                updateDropZoneSizes(null, null); // passing null splits the views evenly
             }
+        } else {
+            // We're already in split so get taskInfo from the controller to populate icon / color.
+            ActivityManager.RunningTaskInfo topOrLeftTask =
+                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT);
+            ActivityManager.RunningTaskInfo bottomOrRightTask =
+                    mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
+            if (topOrLeftTask != null && bottomOrRightTask != null) {
+                Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
+                int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
+                Drawable bottomOrRightIcon = mIconProvider.getIcon(
+                        bottomOrRightTask.topActivityInfo);
+                int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
+                mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
+                mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
+            }
+
+            // Update the dropzones to match existing split sizes
+            Rect topOrLeftBounds = new Rect();
+            Rect bottomOrRightBounds = new Rect();
+            mSplitScreenController.getStageBounds(topOrLeftBounds, bottomOrRightBounds);
+            updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds);
         }
     }
 
+    /**
+     * Sets the size of the two drop zones based on the provided bounds. The divider sits between
+     * the views and its size is included in the calculations.
+     *
+     * @param bounds1 bounds to apply to the first dropzone view, null if split in half.
+     * @param bounds2 bounds to apply to the second dropzone view, null if split in half.
+     */
+    private void updateDropZoneSizes(Rect bounds1, Rect bounds2) {
+        final int orientation = getResources().getConfiguration().orientation;
+        final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT;
+        final int halfDivider = mDividerSize / 2;
+        final LinearLayout.LayoutParams dropZoneView1 =
+                (LayoutParams) mDropZoneView1.getLayoutParams();
+        final LinearLayout.LayoutParams dropZoneView2 =
+                (LayoutParams) mDropZoneView2.getLayoutParams();
+        if (isPortrait) {
+            dropZoneView1.width = MATCH_PARENT;
+            dropZoneView2.width = MATCH_PARENT;
+            dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT;
+            dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT;
+        } else {
+            dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT;
+            dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT;
+            dropZoneView1.height = MATCH_PARENT;
+            dropZoneView2.height = MATCH_PARENT;
+        }
+        dropZoneView1.weight = bounds1 != null ? 0 : 1;
+        dropZoneView2.weight = bounds2 != null ? 0 : 1;
+        mDropZoneView1.setLayoutParams(dropZoneView1);
+        mDropZoneView2.setLayoutParams(dropZoneView2);
+    }
+
     public void show() {
         mIsShowing = true;
         recomputeDropTargets();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index cf4e56e..46c4a40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -186,6 +186,15 @@
         return mStageCoordinator.isSplitScreenVisible();
     }
 
+    @Nullable
+    public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
+        if (isSplitScreenVisible()) {
+            int taskId = mStageCoordinator.getTaskId(splitPosition);
+            return mTaskOrganizer.getRunningTaskInfo(taskId);
+        }
+        return null;
+    }
+
     public boolean isTaskInSplitScreen(int taskId) {
         return isSplitScreenVisible()
                 && mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index cdaa54c..dd27017 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -522,6 +522,14 @@
         return SplitLayout.reversePosition(mSideStagePosition);
     }
 
+    int getTaskId(@SplitPosition int splitPosition) {
+        if (mSideStagePosition == splitPosition) {
+            return mSideStage.getTopVisibleChildTaskId();
+        } else {
+            return mMainStage.getTopVisibleChildTaskId();
+        }
+    }
+
     void setSideStagePosition(@SplitPosition int sideStagePosition,
             @Nullable WindowContainerTransaction wct) {
         setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 8bc1223..185479b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -794,7 +794,7 @@
     }
 
     @Test
-    public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() {
+    public void test_expanded_removeLastBubble_collapsesIfOverflowNotEmpty() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         changeExpandedStateAtTime(true, 2000);
@@ -804,7 +804,7 @@
         mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
         assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0);
-        assertSelectionChangedTo(mBubbleData.getOverflow());
+        assertExpandedChangedTo(false);
     }
 
     @Test
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index 6dc05ad..a2eae2c 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -303,6 +303,11 @@
         public void onCameraClosed(String cameraId) {
             Log.v(TAG, String.format("Camera %s is closed", cameraId));
         }
+        @Override
+        public void onTorchStrengthLevelChanged(String cameraId, int torchStrength) {
+            Log.v(TAG, String.format("Camera " + cameraId + " torch strength level changed to "
+                    + torchStrength ));
+        }
     }
 
     /**
diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml
index c5926a5..06f2d9d 100644
--- a/packages/CompanionDeviceManager/AndroidManifest.xml
+++ b/packages/CompanionDeviceManager/AndroidManifest.xml
@@ -19,10 +19,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.companiondevicemanager">
 
-    <permission
-        android:name="com.android.companiondevicemanager.permission.BIND"
-        android:protectionLevel="signature" />
-
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
@@ -43,23 +39,17 @@
         android:forceQueryable="true"
         android:supportsRtl="true">
 
-        <service
-            android:name=".CompanionDeviceDiscoveryService"
-            android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
-            android:exported="true">
-        </service>
-
         <activity
             android:name=".CompanionDeviceActivity"
-            android:theme="@style/ChooserActivity"
+            android:exported="true"
+            android:launchMode="singleInstance"
+            android:excludeFromRecents="true"
             android:permission="android.permission.BIND_COMPANION_DEVICE_MANAGER_SERVICE"
-            android:exported="true">
-            <!--TODO include url scheme filter similar to PrintSpooler -->
-            <intent-filter>
-                <action android:name="android.companiondevice.START_DISCOVERY" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
+            android:theme="@style/ChooserActivity"/>
+
+        <service
+            android:name=".CompanionDeviceDiscoveryService"
+            android:exported="false" />
 
     </application>
 
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
new file mode 100644
index 0000000..c87bac6
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:background="@drawable/dialog_background"
+              android:elevation="16dp"
+              android:maxHeight="400dp"
+              android:orientation="vertical"
+              android:padding="18dp"
+              android:layout_gravity="center">
+
+    <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:paddingHorizontal="12dp"
+            style="@*android:style/TextAppearance.Widget.Toolbar.Title"/>
+    <!-- style="@*android:style/TextAppearance.Widget.Toolbar.Title" -->
+
+    <TextView
+            android:id="@+id/summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:layout_marginBottom="12dp"
+            android:gravity="center"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textSize="14sp" />
+
+    <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+
+        <ListView
+                android:id="@+id/device_list"
+                style="@android:style/Widget.Material.ListView"
+                android:layout_width="match_parent"
+                android:layout_height="200dp" />
+
+    </RelativeLayout>
+
+    <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="end">
+
+        <Button
+                android:id="@+id/button_cancel"
+                style="@android:style/Widget.Material.Button.Borderless.Colored"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/consent_no"
+                android:textColor="?android:attr/textColorSecondary" />
+
+        <Button
+                android:id="@+id/button_allow"
+                style="@android:style/Widget.Material.Button.Borderless.Colored"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/consent_yes" />
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/buttons.xml b/packages/CompanionDeviceManager/res/layout/buttons.xml
deleted file mode 100644
index a80720c..0000000
--- a/packages/CompanionDeviceManager/res/layout/buttons.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/buttons"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:layout_alignParentBottom="true"
-    android:layout_alignParentEnd="true"
-    android:gravity="end"
->
-    <Button
-        android:id="@+id/button_cancel"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/consent_no"
-        android:textColor="?android:attr/textColorSecondary"
-        style="@android:style/Widget.Material.Button.Borderless.Colored"
-    />
-    <Button
-        android:id="@+id/button_pair"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/consent_yes"
-        style="@android:style/Widget.Material.Button.Borderless.Colored"
-    />
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/device_chooser.xml b/packages/CompanionDeviceManager/res/layout/device_chooser.xml
deleted file mode 100644
index 273347a..0000000
--- a/packages/CompanionDeviceManager/res/layout/device_chooser.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/container"
-    android:layout_height="400dp"
-    style="@style/ContainerLayout"
-    >
-
-    <include layout="@layout/title" />
-
-    <include layout="@layout/profile_summary" />
-
-    <ListView
-        android:id="@+id/device_list"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_below="@+id/profile_summary"
-        android:layout_above="@+id/buttons"
-        style="@android:style/Widget.Material.ListView"
-    />
-
-    <include layout="@layout/buttons" />
-
-</RelativeLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml b/packages/CompanionDeviceManager/res/layout/device_confirmation.xml
deleted file mode 100644
index 1336e79..0000000
--- a/packages/CompanionDeviceManager/res/layout/device_confirmation.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/container"
-    android:layout_height="wrap_content"
-    style="@style/ContainerLayout"
-    >
-
-    <include layout="@layout/title" />
-
-    <include layout="@layout/profile_summary" />
-
-    <include layout="@layout/buttons" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/profile_summary.xml b/packages/CompanionDeviceManager/res/layout/profile_summary.xml
deleted file mode 100644
index 80fec59..0000000
--- a/packages/CompanionDeviceManager/res/layout/profile_summary.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2020 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.
-  -->
-
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/profile_summary"
-    android:layout_below="@+id/title"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_marginStart="16dp"
-    android:layout_marginEnd="16dp"
-    android:textColor="?android:attr/textColorSecondary"
-    android:textSize="14sp"
-    android:gravity="center"
-/>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/title.xml b/packages/CompanionDeviceManager/res/layout/title.xml
deleted file mode 100644
index 9a50366..0000000
--- a/packages/CompanionDeviceManager/res/layout/title.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/title"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center"
-    style="@*android:style/TextAppearance.Widget.Toolbar.Title"
-/>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/dimens.xml b/packages/CompanionDeviceManager/res/values/dimens.xml
deleted file mode 100644
index da7b0d1..0000000
--- a/packages/CompanionDeviceManager/res/values/dimens.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-    <!--  Padding applied on most UI elements  -->
-    <dimen name="padding">12dp</dimen>
-
-</resources>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 44748e9..cb8b616 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -19,25 +19,58 @@
     <!-- Title of the CompanionDeviceManager application. [CHAR LIMIT=50] -->
     <string name="app_label">Companion Device Manager</string>
 
-    <!-- Title of the device selection dialog. -->
-    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <!-- Title of the device association confirmation dialog. -->
+    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
 
-    <!-- The generic placeholder for a device type when nothing specific is known about it [CHAR LIMIT=30] -->
-    <string name="profile_name_generic">device</string>
+    <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
 
     <!-- The name of the "watch" device type [CHAR LIMIT=30] -->
     <string name="profile_name_watch">watch</string>
 
-    <!-- Title of the device association confirmation dialog. -->
-    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <!-- Title of the device selection dialog. -->
+    <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
 
-    <!-- Text of the device profile permissions explanation in the association dialog. -->
-    <string name="profile_summary">This app is needed to manage your <xliff:g id="profile_name" example="watch">%1$s</xliff:g>. <xliff:g id="privileges_discplaimer" example="Android Wear will get access to your Notifications, Calendar and Contacts.">%2$s</xliff:g></string>
+    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+
+    <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
+
+    <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="title_app_streaming">Allow &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to stream applications?</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="default">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this phone when connected.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="tablet">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this tablet when connected.</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_app_streaming" product="device">Let &lt;strong&gt;<xliff:g id="app_name" example="Exo">%1$s</xliff:g>&lt;/strong&gt; to provide &lt;strong&gt;<xliff:g id="device_name" example="Pixelbook Go">%2$s</xliff:g>&lt;/strong&gt; remote access to access to applications installed on this device when connected.</string>
+
+    <!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= -->
+
+    <!-- Confirmation for associating an application with a companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
+    <string name="title_automotive_projection"></string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of AUTOMOTIVE_PROJECTION profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_automotive_projection"></string>
+
+    <!-- ================= null profile ================= -->
+
+    <!-- A noun for a companion device with unspecified profile (type) [CHAR LIMIT=30] -->
+    <string name="profile_name_generic">device</string>
+
+    <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
+    <string name="summary_generic"></string>
+
+    <!-- ================= Buttons ================= -->
 
     <!-- Positive button for the device-app association consent dialog [CHAR LIMIT=30] -->
     <string name="consent_yes">Allow</string>
 
     <!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] -->
     <string name="consent_no">Don\u2019t allow</string>
-
 </resources>
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
deleted file mode 100644
index 9dced47b..0000000
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-<resources>
-    <style name="ContainerLayout">
-        <item name="android:orientation">vertical</item>
-        <item name="android:layout_width">match_parent</item>
-        <item name="android:elevation">16dp</item>
-        <item name="android:background">@drawable/dialog_background</item>
-        <item name="android:paddingTop">18dip</item>
-        <item name="android:paddingStart">20dip</item>
-        <item name="android:paddingEnd">16dip</item>
-        <item name="android:paddingBottom">16dip</item>
-        <item name="android:layout_gravity">center</item>
-    </style>
-</resources>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index a5168cc..cc887c3 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -16,327 +16,356 @@
 
 package com.android.companiondevicemanager;
 
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
-import static android.text.TextUtils.emptyIfNull;
-import static android.text.TextUtils.isEmpty;
-import static android.text.TextUtils.withoutPrefix;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.AssociationRequest.DEVICE_PROFILE_WATCH;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import static com.android.companiondevicemanager.Utils.getApplicationLabel;
+import static com.android.companiondevicemanager.Utils.getHtmlFromResources;
+import static com.android.companiondevicemanager.Utils.prepareResultReceiverForIpc;
+
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
+import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
+import android.companion.IAssociationRequestCallback;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.database.DataSetObserver;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
+import android.net.MacAddress;
 import android.os.Bundle;
-import android.text.Html;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.text.Spanned;
 import android.util.Log;
-import android.util.SparseArray;
-import android.util.TypedValue;
-import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
+import android.widget.Button;
 import android.widget.ListView;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 
-import com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DeviceFilterPair;
-import com.android.internal.util.Preconditions;
-
 public class CompanionDeviceActivity extends Activity {
-
     private static final boolean DEBUG = false;
-    private static final String LOG_TAG = CompanionDeviceActivity.class.getSimpleName();
+    private static final String TAG = CompanionDeviceActivity.class.getSimpleName();
 
-    static CompanionDeviceActivity sInstance;
+    // Keep the following constants in sync with
+    // frameworks/base/services/companion/java/
+    // com/android/server/companion/AssociationRequestsProcessor.java
 
-    View mLoadingIndicator = null;
-    ListView mDeviceListView;
-    private View mPairButton;
-    private View mCancelButton;
+    // AssociationRequestsProcessor <-> UI
+    private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
+    private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
 
-    DevicesAdapter mDevicesAdapter;
+    // AssociationRequestsProcessor -> UI
+    private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
+    private static final String EXTRA_ASSOCIATION = "association";
+
+    // UI -> AssociationRequestsProcessor
+    private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
+    private static final String EXTRA_MAC_ADDRESS = "mac_address";
+
+    private AssociationRequest mRequest;
+    private IAssociationRequestCallback mAppCallback;
+    private ResultReceiver mCdmServiceReceiver;
+
+    // Always present widgets.
+    private TextView mTitle;
+    private TextView mSummary;
+
+    // Progress indicator is only shown while we are looking for the first suitable device for a
+    // "regular" (ie. not self-managed) association.
+    private View mProgressIndicator;
+
+    // Present for self-managed association requests and "single-device" regular association
+    // regular.
+    private Button mButtonAllow;
+
+    // The list is only shown for multiple-device regular association request, after at least one
+    // matching device is found.
+    private @Nullable ListView mListView;
+    private @Nullable DeviceListAdapter mAdapter;
+
+    // The flag used to prevent double taps, that may lead to sending several requests for creating
+    // an association to CDM.
+    private boolean mAssociationApproved;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        Log.i(LOG_TAG, "Starting UI for " + getService().mRequest);
-
-        if (getService().mDevicesFound.isEmpty()) {
-            Log.e(LOG_TAG, "About to show UI, but no devices to show");
-        }
-
         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
-        sInstance = this;
-        getService().mActivity = this;
-
-        String deviceProfile = getRequest().getDeviceProfile();
-        String profilePrivacyDisclaimer = emptyIfNull(getRequest()
-                .getDeviceProfilePrivilegesDescription())
-                .replace("APP_NAME", getCallingAppName());
-        boolean useDeviceProfile = deviceProfile != null && !isEmpty(profilePrivacyDisclaimer);
-        String profileName = useDeviceProfile
-                ? getDeviceProfileName(deviceProfile)
-                : getString(R.string.profile_name_generic);
-
-        if (getRequest().isSingleDevice()) {
-            setContentView(R.layout.device_confirmation);
-            final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0);
-            setTitle(Html.fromHtml(getString(
-                    R.string.confirmation_title,
-                    Html.escapeHtml(getCallingAppName()),
-                    Html.escapeHtml(selectedDevice.getDisplayName())), 0));
-
-            mPairButton = findViewById(R.id.button_pair);
-            mPairButton.setOnClickListener(v -> onDeviceConfirmed(getService().mSelectedDevice));
-            getService().mSelectedDevice = selectedDevice;
-            onSelectionUpdate();
-            if (getRequest().isSkipPrompt()) {
-                onDeviceConfirmed(selectedDevice);
-            }
-        } else {
-            setContentView(R.layout.device_chooser);
-            mPairButton = findViewById(R.id.button_pair);
-            mPairButton.setVisibility(View.GONE);
-            setTitle(Html.fromHtml(getString(R.string.chooser_title,
-                    Html.escapeHtml(profileName),
-                    Html.escapeHtml(getCallingAppName())), 0));
-            mDeviceListView = findViewById(R.id.device_list);
-            mDevicesAdapter = new DevicesAdapter();
-            mDeviceListView.setAdapter(mDevicesAdapter);
-            mDeviceListView.setOnItemClickListener((adapterView, view, pos, l) -> {
-                getService().mSelectedDevice =
-                        (DeviceFilterPair) adapterView.getItemAtPosition(pos);
-                mDevicesAdapter.notifyDataSetChanged();
-            });
-            mDevicesAdapter.registerDataSetObserver(new DataSetObserver() {
-                @Override
-                public void onChanged() {
-                    onSelectionUpdate();
-                }
-            });
-            mDeviceListView.addFooterView(mLoadingIndicator = getProgressBar(), null, false);
-        }
-
-        TextView profileSummary = findViewById(R.id.profile_summary);
-
-        if (useDeviceProfile) {
-            profileSummary.setVisibility(View.VISIBLE);
-            String deviceRef = getRequest().isSingleDevice()
-                    ? getService().mDevicesFound.get(0).getDisplayName()
-                    : profileName;
-            profileSummary.setText(getString(R.string.profile_summary,
-                    deviceRef,
-                    profilePrivacyDisclaimer));
-        } else {
-            profileSummary.setVisibility(View.GONE);
-        }
-
-        mCancelButton = findViewById(R.id.button_cancel);
-        mCancelButton.setOnClickListener(v -> cancel());
     }
 
-    static void notifyDevicesChanged() {
-        if (sInstance != null && sInstance.mDevicesAdapter != null && !sInstance.isFinishing()) {
-            sInstance.mDevicesAdapter.notifyDataSetChanged();
-        }
-    }
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (DEBUG) Log.d(TAG, "onStart()");
 
-    private AssociationRequest getRequest() {
-        return getService().mRequest;
-    }
+        final Intent intent = getIntent();
+        mRequest = intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST);
+        mAppCallback = IAssociationRequestCallback.Stub.asInterface(
+                intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK));
+        mCdmServiceReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
 
-    private String getDeviceProfileName(@Nullable String deviceProfile) {
-        if (deviceProfile == null) {
-            return getString(R.string.profile_name_generic);
-        }
-        switch (deviceProfile) {
-            case AssociationRequest.DEVICE_PROFILE_WATCH: {
-                return getString(R.string.profile_name_watch);
-            }
-            default: {
-                Log.w(LOG_TAG,
-                        "No localized profile name found for device profile: " + deviceProfile);
-                return withoutPrefix("android.app.role.COMPANION_DEVICE_", deviceProfile)
-                        .toLowerCase()
-                        .replace('_', ' ');
-            }
-        }
-    }
+        requireNonNull(mRequest);
+        requireNonNull(mAppCallback);
+        requireNonNull(mCdmServiceReceiver);
 
-    private void cancel() {
-        Log.i(LOG_TAG, "cancel()");
-        getService().onCancel();
-        setResult(RESULT_CANCELED);
-        finish();
+        // Start discovery services if needed.
+        if (!mRequest.isSelfManaged()) {
+            CompanionDeviceDiscoveryService.startForRequest(this, mRequest);
+        }
+        // Init UI.
+        initUI();
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        if (!isFinishing() && !isChangingConfigurations()) {
-            Log.i(LOG_TAG, "onStop() - cancelling");
-            cancel();
+        if (DEBUG) Log.d(TAG, "onStop(), finishing=" + isFinishing());
+
+        // TODO: handle config changes without cancelling.
+        if (!isFinishing()) {
+            cancel(); // will finish()
         }
+
+        // mAdapter may be observing - need to remove it.
+        CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.deleteObservers();
     }
 
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        getService().mActivity = null;
-        if (sInstance == this) {
-            sInstance = null;
-        }
-    }
+    protected void onNewIntent(Intent intent) {
+        // Handle another incoming request (while we are not done with the original - mRequest -
+        // yet).
+        final AssociationRequest request = requireNonNull(
+                intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST));
+        if (DEBUG) Log.d(TAG, "onNewIntent(), request=" + request);
 
-    private CharSequence getCallingAppName() {
+        // We can only "process" one request at a time.
+        final IAssociationRequestCallback appCallback = IAssociationRequestCallback.Stub
+                .asInterface(intent.getExtras().getBinder(EXTRA_APPLICATION_CALLBACK));
         try {
-            final PackageManager packageManager = getPackageManager();
-            String callingPackage = Preconditions.checkStringNotEmpty(
-                    getCallingPackage(),
-                    "This activity must be called for result");
-            return packageManager.getApplicationLabel(
-                    packageManager.getApplicationInfo(callingPackage, 0));
-        } catch (PackageManager.NameNotFoundException e) {
-            throw new RuntimeException(e);
+            requireNonNull(appCallback).onFailure("Busy.");
+        } catch (RemoteException ignore) {
         }
     }
 
-    @Override
-    public String getCallingPackage() {
-        return requireNonNull(getRequest().getCallingPackage());
-    }
+    private void initUI() {
+        if (DEBUG) Log.d(TAG, "initUI(), request=" + mRequest);
 
-    @Override
-    public void setTitle(CharSequence title) {
-        final TextView titleView = findViewById(R.id.title);
-        final int padding = getPadding(getResources());
-        titleView.setPadding(padding, padding, padding, padding);
-        titleView.setText(title);
-    }
+        setContentView(R.layout.activity_confirmation);
 
-    private ProgressBar getProgressBar() {
-        final ProgressBar progressBar = new ProgressBar(this);
-        progressBar.setForegroundGravity(Gravity.CENTER_HORIZONTAL);
-        final int padding = getPadding(getResources());
-        progressBar.setPadding(padding, padding, padding, padding);
-        return progressBar;
-    }
+        mTitle = findViewById(R.id.title);
+        mSummary = findViewById(R.id.summary);
 
-    static int getPadding(Resources r) {
-        return r.getDimensionPixelSize(R.dimen.padding);
-    }
+        mListView = findViewById(R.id.device_list);
+        mListView.setOnItemClickListener((av, iv, position, id) -> onListItemClick(position));
 
-    private void onSelectionUpdate() {
-        DeviceFilterPair selectedDevice = getService().mSelectedDevice;
-        if (mPairButton.getVisibility() != View.VISIBLE && selectedDevice != null) {
-            onDeviceConfirmed(selectedDevice);
+        mButtonAllow = findViewById(R.id.button_allow);
+        mButtonAllow.setOnClickListener(this::onAllowButtonClick);
+
+        findViewById(R.id.button_cancel).setOnClickListener(v -> cancel());
+
+        final CharSequence appLabel = getApplicationLabel(this, mRequest.getPackageName());
+        if (mRequest.isSelfManaged()) {
+            initUiForSelfManagedAssociation(appLabel);
+        } else if (mRequest.isSingleDevice()) {
+            initUiForSingleDevice(appLabel);
         } else {
-            mPairButton.setEnabled(selectedDevice != null);
+            initUiForMultipleDevices(appLabel);
         }
     }
 
-    private CompanionDeviceDiscoveryService getService() {
-        return CompanionDeviceDiscoveryService.sInstance;
+    private void onAssociationCreated(@NonNull AssociationInfo association) {
+        if (DEBUG) Log.i(TAG, "onAssociationCreated(), association=" + association);
+
+        // Don't need to notify the app, CdmService has already done that. Just finish.
+        setResultAndFinish(association);
     }
 
-    protected void onDeviceConfirmed(DeviceFilterPair selectedDevice) {
-        Log.i(LOG_TAG, "onDeviceConfirmed(selectedDevice = " + selectedDevice + ")");
-        getService().onDeviceSelected(
-                getCallingPackage(), getDeviceMacAddress(selectedDevice.device));
+    private void cancel() {
+        if (DEBUG) Log.i(TAG, "cancel()");
+
+        // Stop discovery service if it was used.
+        if (!mRequest.isSelfManaged()) {
+            CompanionDeviceDiscoveryService.stop(this);
+        }
+
+        // First send callback to the app directly...
+        try {
+            mAppCallback.onFailure("Cancelled.");
+        } catch (RemoteException ignore) {
+        }
+
+        // ... then set result and finish ("sending" onActivityResult()).
+        setResultAndFinish(null);
     }
 
-    void setResultAndFinish() {
-        Log.i(LOG_TAG, "setResultAndFinish(selectedDevice = "
-                + getService().mSelectedDevice.device + ")");
-        setResult(RESULT_OK,
-                new Intent().putExtra(
-                        CompanionDeviceManager.EXTRA_DEVICE, getService().mSelectedDevice.device));
+    private void setResultAndFinish(@Nullable AssociationInfo association) {
+        if (DEBUG) Log.i(TAG, "setResultAndFinish(), association=" + association);
+
+        final Intent data = new Intent();
+        if (association != null) {
+            data.putExtra(CompanionDeviceManager.EXTRA_ASSOCIATION, association);
+            if (!association.isSelfManaged()) {
+                data.putExtra(CompanionDeviceManager.EXTRA_DEVICE,
+                        association.getDeviceMacAddressAsString());
+            }
+        }
+        setResult(association != null ? RESULT_OK : RESULT_CANCELED, data);
+
         finish();
     }
 
-    class DevicesAdapter extends BaseAdapter {
-        private final Drawable mBluetoothIcon = icon(android.R.drawable.stat_sys_data_bluetooth);
-        private final Drawable mWifiIcon = icon(com.android.internal.R.drawable.ic_wifi_signal_3);
+    private void initUiForSelfManagedAssociation(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_SelfManaged_Association()");
 
-        private SparseArray<Integer> mColors = new SparseArray();
+        final CharSequence deviceName = mRequest.getDisplayName(); // "<device>";
+        final String deviceProfile = mRequest.getDeviceProfile(); // DEVICE_PROFILE_APP_STREAMING;
 
-        private Drawable icon(int drawableRes) {
-            Drawable icon = getResources().getDrawable(drawableRes, null);
-            icon.setTint(Color.DKGRAY);
-            return icon;
+        final Spanned title;
+        final Spanned summary;
+        switch (deviceProfile) {
+            case DEVICE_PROFILE_APP_STREAMING:
+                title = getHtmlFromResources(this, R.string.title_app_streaming, appLabel);
+                summary = getHtmlFromResources(
+                        this, R.string.summary_app_streaming, appLabel, deviceName);
+                break;
+
+            case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION:
+                title = getHtmlFromResources(this, R.string.title_automotive_projection, appLabel);
+                summary = getHtmlFromResources(
+                        this, R.string.summary_automotive_projection, appLabel, deviceName);
+                break;
+
+            default:
+                throw new RuntimeException("Unsupported profile " + deviceProfile);
         }
+        mTitle.setText(title);
+        mSummary.setText(summary);
 
-        @Override
-        public View getView(
-                int position,
-                @Nullable View convertView,
-                @NonNull ViewGroup parent) {
-            TextView view = convertView instanceof TextView
-                    ? (TextView) convertView
-                    : newView();
-            bind(view, getItem(position));
-            return view;
-        }
-
-        private void bind(TextView textView, DeviceFilterPair device) {
-            textView.setText(device.getDisplayName());
-            textView.setBackgroundColor(
-                    device.equals(getService().mSelectedDevice)
-                            ? getColor(android.R.attr.colorControlHighlight)
-                            : Color.TRANSPARENT);
-            textView.setCompoundDrawablesWithIntrinsicBounds(
-                    device.device instanceof android.net.wifi.ScanResult
-                            ? mWifiIcon
-                            : mBluetoothIcon,
-                    null, null, null);
-            textView.getCompoundDrawables()[0].setTint(getColor(android.R.attr.colorForeground));
-        }
-
-        private TextView newView() {
-            final TextView textView = new TextView(CompanionDeviceActivity.this);
-            textView.setTextColor(getColor(android.R.attr.colorForeground));
-            final int padding = CompanionDeviceActivity.getPadding(getResources());
-            textView.setPadding(padding, padding, padding, padding);
-            textView.setCompoundDrawablePadding(padding);
-            return textView;
-        }
-
-        private int getColor(int colorAttr) {
-            if (mColors.contains(colorAttr)) {
-                return mColors.get(colorAttr);
-            }
-            TypedValue typedValue = new TypedValue();
-            TypedArray a = obtainStyledAttributes(typedValue.data, new int[] { colorAttr });
-            int result = a.getColor(0, 0);
-            a.recycle();
-            mColors.put(colorAttr, result);
-            return result;
-        }
-
-        @Override
-        public int getCount() {
-            return getService().mDevicesFound.size();
-        }
-
-        @Override
-        public DeviceFilterPair getItem(int position) {
-            return getService().mDevicesFound.get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
+        mListView.setVisibility(View.GONE);
     }
+
+    private void initUiForSingleDevice(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_SingleDevice()");
+
+        // TODO: use real name
+        final String deviceName = "<device>";
+        final String deviceProfile = mRequest.getDeviceProfile();
+
+        final Spanned title = getHtmlFromResources(
+                this, R.string.confirmation_title, appLabel, deviceName);
+        final Spanned summary;
+        if (deviceProfile == null) {
+            summary = getHtmlFromResources(this, R.string.summary_generic);
+        } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
+            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel, deviceName);
+        } else {
+            throw new RuntimeException("Unsupported profile " + deviceProfile);
+        }
+
+        mTitle.setText(title);
+        mSummary.setText(summary);
+
+        mListView.setVisibility(View.GONE);
+    }
+
+    private void initUiForMultipleDevices(CharSequence appLabel) {
+        if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()");
+
+        final String deviceProfile = mRequest.getDeviceProfile();
+
+        final String profileName;
+        final Spanned summary;
+        if (deviceProfile == null) {
+            profileName = getString(R.string.profile_name_generic);
+            summary = getHtmlFromResources(this, R.string.summary_generic);
+        } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
+            profileName = getString(R.string.profile_name_watch);
+            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel);
+        } else {
+            throw new RuntimeException("Unsupported profile " + deviceProfile);
+        }
+        final Spanned title = getHtmlFromResources(
+                this, R.string.chooser_title, profileName, appLabel);
+
+        mTitle.setText(title);
+        mSummary.setText(summary);
+
+        mAdapter = new DeviceListAdapter(this);
+        CompanionDeviceDiscoveryService.SCAN_RESULTS_OBSERVABLE.addObserver(mAdapter);
+        // TODO: hide the list and show a spinner until a first device matching device is found.
+        mListView.setAdapter(mAdapter);
+
+        // "Remove" consent button: users would need to click on the list item.
+        mButtonAllow.setVisibility(View.GONE);
+    }
+
+    private void onListItemClick(int position) {
+        if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
+
+        final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position);
+        final MacAddress macAddress = selectedDevice.getMacAddress();
+        onAssociationApproved(macAddress);
+    }
+
+    private void onAllowButtonClick(View v) {
+        if (DEBUG) Log.d(TAG, "onAllowButtonClick()");
+
+        // Disable the button, to prevent more clicks.
+        v.setEnabled(false);
+
+        final MacAddress macAddress;
+        if (mRequest.isSelfManaged()) {
+            macAddress = null;
+        } else {
+            // TODO: implement.
+            throw new UnsupportedOperationException(
+                    "isSingleDevice() requests are not supported yet.");
+        }
+        onAssociationApproved(macAddress);
+    }
+
+    private void onAssociationApproved(@Nullable MacAddress macAddress) {
+        if (mAssociationApproved) return;
+        mAssociationApproved = true;
+
+        if (DEBUG) Log.i(TAG, "onAssociationApproved() macAddress=" + macAddress);
+
+        if (!mRequest.isSelfManaged()) {
+            requireNonNull(macAddress);
+            CompanionDeviceDiscoveryService.stop(this);
+        }
+
+        final Bundle data = new Bundle();
+        data.putParcelable(EXTRA_ASSOCIATION_REQUEST, mRequest);
+        data.putBinder(EXTRA_APPLICATION_CALLBACK, mAppCallback.asBinder());
+        if (macAddress != null) {
+            data.putParcelable(EXTRA_MAC_ADDRESS, macAddress);
+        }
+
+        data.putParcelable(EXTRA_RESULT_RECEIVER,
+                prepareResultReceiverForIpc(mOnAssociationCreatedReceiver));
+
+        mCdmServiceReceiver.send(RESULT_CODE_ASSOCIATION_APPROVED, data);
+    }
+
+    private final ResultReceiver mOnAssociationCreatedReceiver =
+            new ResultReceiver(Handler.getMain()) {
+                @Override
+                protected void onReceiveResult(int resultCode, Bundle data) {
+                    if (resultCode != RESULT_CODE_ASSOCIATION_CREATED) {
+                        throw new RuntimeException("Unknown result code: " + resultCode);
+                    }
+
+                    final AssociationInfo association = data.getParcelable(EXTRA_ASSOCIATION);
+                    requireNonNull(association);
+
+                    onAssociationCreated(association);
+                }
+            };
 }
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index 126b823..a4ff1dc 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -16,18 +16,17 @@
 
 package com.android.companiondevicemanager;
 
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
-import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
-
+import static com.android.companiondevicemanager.Utils.runOnMainThread;
 import static com.android.internal.util.ArrayUtils.isEmpty;
-import static com.android.internal.util.CollectionUtils.emptyIfNull;
-import static com.android.internal.util.CollectionUtils.size;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.internal.util.CollectionUtils.filter;
+import static com.android.internal.util.CollectionUtils.find;
+import static com.android.internal.util.CollectionUtils.map;
+
+import static java.util.Objects.requireNonNull;
 
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.PendingIntent;
 import android.app.Service;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -42,8 +41,6 @@
 import android.companion.BluetoothDeviceFilter;
 import android.companion.BluetoothLeDeviceFilter;
 import android.companion.DeviceFilter;
-import android.companion.IAssociationRequestCallback;
-import android.companion.ICompanionDeviceDiscoveryService;
 import android.companion.WifiDeviceFilter;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -53,417 +50,411 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Parcelable;
-import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.Preconditions;
-
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Observable;
 
 public class CompanionDeviceDiscoveryService extends Service {
-
     private static final boolean DEBUG = false;
-    private static final String LOG_TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
+    private static final String TAG = CompanionDeviceDiscoveryService.class.getSimpleName();
 
-    private static final long SCAN_TIMEOUT = 20000;
+    private static final String ACTION_START_DISCOVERY =
+            "com.android.companiondevicemanager.action.START_DISCOVERY";
+    private static final String ACTION_STOP_DISCOVERY =
+            "com.android.companiondevicemanager.action.ACTION_STOP_DISCOVERY";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
 
-    static CompanionDeviceDiscoveryService sInstance;
+    private static final long SCAN_TIMEOUT = 20_000L; // 20 seconds
 
-    private BluetoothManager mBluetoothManager;
-    private BluetoothAdapter mBluetoothAdapter;
+    // TODO: replace with LiveData-s?
+    static final Observable TIMEOUT_OBSERVABLE = new MyObservable();
+    static final Observable SCAN_RESULTS_OBSERVABLE = new MyObservable();
+
+    private static CompanionDeviceDiscoveryService sInstance;
+
+    private BluetoothManager mBtManager;
+    private BluetoothAdapter mBtAdapter;
     private WifiManager mWifiManager;
-    @Nullable private BluetoothLeScanner mBLEScanner;
-    private ScanSettings mDefaultScanSettings = new ScanSettings.Builder()
-            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-            .build();
+    private BluetoothLeScanner mBleScanner;
 
-    private List<DeviceFilter<?>> mFilters;
-    private List<BluetoothLeDeviceFilter> mBLEFilters;
-    private List<BluetoothDeviceFilter> mBluetoothFilters;
-    private List<WifiDeviceFilter> mWifiFilters;
-    private List<ScanFilter> mBLEScanFilters;
+    private ScanCallback mBleScanCallback;
+    private BluetoothBroadcastReceiver mBtReceiver;
+    private WifiBroadcastReceiver mWifiReceiver;
 
-    AssociationRequest mRequest;
-    List<DeviceFilterPair> mDevicesFound;
-    DeviceFilterPair mSelectedDevice;
-    IAssociationRequestCallback mApplicationCallback;
+    private boolean mDiscoveryStarted = false;
+    private boolean mDiscoveryStopped = false;
+    private final List<DeviceFilterPair<?>> mDevicesFound = new ArrayList<>();
 
-    AndroidFuture<String> mServiceCallback;
-    boolean mIsScanning = false;
-    @Nullable
-    CompanionDeviceActivity mActivity = null;
+    private final Runnable mTimeoutRunnable = this::timeout;
 
-    private final ICompanionDeviceDiscoveryService mBinder =
-            new ICompanionDeviceDiscoveryService.Stub() {
-        @Override
-        public void startDiscovery(AssociationRequest request,
-                String callingPackage,
-                IAssociationRequestCallback appCallback,
-                AndroidFuture<String> serviceCallback) {
-            Log.i(LOG_TAG,
-                    "startDiscovery() called with: filter = [" + request
-                            + "], appCallback = [" + appCallback + "]"
-                            + "], serviceCallback = [" + serviceCallback + "]");
-            mApplicationCallback = appCallback;
-            mServiceCallback = serviceCallback;
-            Handler.getMain().sendMessage(obtainMessage(
-                    CompanionDeviceDiscoveryService::startDiscovery,
-                    CompanionDeviceDiscoveryService.this, request));
-        }
+    static void startForRequest(
+            @NonNull Context context, @NonNull AssociationRequest associationRequest) {
+        requireNonNull(associationRequest);
+        final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
+        intent.setAction(ACTION_START_DISCOVERY);
+        intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest);
+        context.startService(intent);
+    }
 
-        @Override
-        public void onAssociationCreated() {
-            Handler.getMain().post(CompanionDeviceDiscoveryService.this::onAssociationCreated);
-        }
-    };
+    static void stop(@NonNull Context context) {
+        final Intent intent = new Intent(context, CompanionDeviceDiscoveryService.class);
+        intent.setAction(ACTION_STOP_DISCOVERY);
+        context.startService(intent);
+    }
 
-    private ScanCallback mBLEScanCallback;
-    private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
-    private WifiBroadcastReceiver mWifiBroadcastReceiver;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        Log.i(LOG_TAG, "onBind(" + intent + ")");
-        return mBinder.asBinder();
+    @MainThread
+    static @NonNull List<DeviceFilterPair<?>> getScanResults() {
+        return sInstance != null ? new ArrayList<>(sInstance.mDevicesFound)
+                : Collections.emptyList();
     }
 
     @Override
     public void onCreate() {
         super.onCreate();
-
-        Log.i(LOG_TAG, "onCreate()");
-
-        mBluetoothManager = getSystemService(BluetoothManager.class);
-        mBluetoothAdapter = mBluetoothManager.getAdapter();
-        mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
-        mWifiManager = getSystemService(WifiManager.class);
-
-        mDevicesFound = new ArrayList<>();
+        if (DEBUG) Log.d(TAG, "onCreate()");
 
         sInstance = this;
-    }
 
-    @MainThread
-    private void startDiscovery(AssociationRequest request) {
-        if (!request.equals(mRequest)) {
-            mRequest = request;
-
-            mFilters = request.getDeviceFilters();
-            mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class);
-            mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class);
-            mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class);
-            mBLEScanFilters
-                    = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter);
-
-            reset();
-        } else {
-            Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request);
-        }
-
-        if (!ArrayUtils.isEmpty(mDevicesFound)) {
-            onReadyToShowUI();
-        }
-
-        // If filtering to get single device by mac address, also search in the set of already
-        // bonded devices to allow linking those directly
-        String singleMacAddressFilter = null;
-        if (mRequest.isSingleDevice()) {
-            int numFilters = size(mBluetoothFilters);
-            for (int i = 0; i < numFilters; i++) {
-                BluetoothDeviceFilter filter = mBluetoothFilters.get(i);
-                if (!TextUtils.isEmpty(filter.getAddress())) {
-                    singleMacAddressFilter = filter.getAddress();
-                    break;
-                }
-            }
-        }
-        if (singleMacAddressFilter != null) {
-            for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-            for (BluetoothDevice dev : emptyIfNull(
-                    mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT))) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-            for (BluetoothDevice dev : emptyIfNull(
-                    mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER))) {
-                onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
-            }
-        }
-
-        if (shouldScan(mBluetoothFilters)) {
-            final IntentFilter intentFilter = new IntentFilter();
-            intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
-
-            Log.i(LOG_TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
-            mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
-            registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
-            mBluetoothAdapter.startDiscovery();
-        }
-
-        if (shouldScan(mBLEFilters) && mBLEScanner != null) {
-            Log.i(LOG_TAG, "BLEScanner.startScan");
-            mBLEScanCallback = new BLEScanCallback();
-            mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
-        }
-
-        if (shouldScan(mWifiFilters)) {
-            Log.i(LOG_TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
-            mWifiBroadcastReceiver = new WifiBroadcastReceiver();
-            registerReceiver(mWifiBroadcastReceiver,
-                    new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
-            mWifiManager.startScan();
-        }
-        mIsScanning = true;
-        Handler.getMain().sendMessageDelayed(
-                obtainMessage(CompanionDeviceDiscoveryService::stopScan, this),
-                SCAN_TIMEOUT);
-    }
-
-    @MainThread
-    private void onAssociationCreated() {
-        mActivity.setResultAndFinish();
-    }
-
-    private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
-        return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
-    }
-
-    @MainThread
-    private void reset() {
-        Log.i(LOG_TAG, "reset()");
-        stopScan();
-        mDevicesFound.clear();
-        mSelectedDevice = null;
-        CompanionDeviceActivity.notifyDevicesChanged();
+        mBtManager = getSystemService(BluetoothManager.class);
+        mBtAdapter = mBtManager.getAdapter();
+        mBleScanner = mBtAdapter.getBluetoothLeScanner();
+        mWifiManager = getSystemService(WifiManager.class);
     }
 
     @Override
-    public boolean onUnbind(Intent intent) {
-        Log.i(LOG_TAG, "onUnbind(intent = " + intent + ")");
-        stopScan();
-        return super.onUnbind(intent);
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        final String action = intent.getAction();
+        if (DEBUG) Log.d(TAG, "onStartCommand() action=" + action);
+
+        switch (action) {
+            case ACTION_START_DISCOVERY:
+                final AssociationRequest request =
+                        intent.getParcelableExtra(EXTRA_ASSOCIATION_REQUEST);
+                startDiscovery(request);
+                break;
+
+            case ACTION_STOP_DISCOVERY:
+                stopDiscoveryAndFinish();
+                break;
+        }
+        return START_NOT_STICKY;
     }
 
-    private void stopScan() {
-        Log.i(LOG_TAG, "stopScan()");
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (DEBUG) Log.d(TAG, "onDestroy()");
 
-        if (!mIsScanning) return;
-        mIsScanning = false;
-
-        if (mActivity != null && mActivity.mDeviceListView != null) {
-            mActivity.mDeviceListView.removeFooterView(mActivity.mLoadingIndicator);
-        }
-
-        mBluetoothAdapter.cancelDiscovery();
-        if (mBluetoothBroadcastReceiver != null) {
-            unregisterReceiver(mBluetoothBroadcastReceiver);
-            mBluetoothBroadcastReceiver = null;
-        }
-        if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
-        if (mWifiBroadcastReceiver != null) {
-            unregisterReceiver(mWifiBroadcastReceiver);
-            mWifiBroadcastReceiver = null;
-        }
-    }
-
-    private void onDeviceFound(@Nullable DeviceFilterPair device) {
-        if (device == null) return;
-
-        Handler.getMain().sendMessage(obtainMessage(
-                CompanionDeviceDiscoveryService::onDeviceFoundMainThread, this, device));
+        sInstance = null;
     }
 
     @MainThread
-    void onDeviceFoundMainThread(@NonNull DeviceFilterPair device) {
-        if (mDevicesFound.contains(device)) {
-            Log.i(LOG_TAG, "Skipping device " + device + " - already among found devices");
-            return;
-        }
+    private void startDiscovery(@NonNull AssociationRequest request) {
+        if (DEBUG) Log.i(TAG, "startDiscovery() request=" + request);
+        requireNonNull(request);
 
-        Log.i(LOG_TAG, "Found device " + device);
+        if (mDiscoveryStarted) throw new RuntimeException("Discovery in progress.");
+        mDiscoveryStarted = true;
 
-        if (mDevicesFound.isEmpty()) {
-            onReadyToShowUI();
-        }
-        mDevicesFound.add(device);
-        CompanionDeviceActivity.notifyDevicesChanged();
-    }
+        final List<DeviceFilter<?>> allFilters = request.getDeviceFilters();
+        final List<BluetoothDeviceFilter> btFilters =
+                filter(allFilters, BluetoothDeviceFilter.class);
+        final List<BluetoothLeDeviceFilter> bleFilters =
+                filter(allFilters, BluetoothLeDeviceFilter.class);
+        final List<WifiDeviceFilter> wifiFilters = filter(allFilters, WifiDeviceFilter.class);
 
-    //TODO also, on timeout -> call onFailure
-    private void onReadyToShowUI() {
-        try {
-            mApplicationCallback.onAssociationPending(PendingIntent.getActivity(
-                    this, 0,
-                    new Intent(this, CompanionDeviceActivity.class),
-                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
-                            | PendingIntent.FLAG_IMMUTABLE));
-        } catch (RemoteException e) {
-            throw new RuntimeException(e);
-        }
-    }
+        checkBoundDevicesIfNeeded(request, btFilters);
 
-    private void onDeviceLost(@Nullable DeviceFilterPair device) {
-        Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
-        Handler.getMain().sendMessage(obtainMessage(
-                CompanionDeviceDiscoveryService::onDeviceLostMainThread, this, device));
+        // If no filters are specified: look for everything.
+        final boolean forceStartScanningAll = isEmpty(allFilters);
+        // Start BT scanning (if needed)
+        mBtReceiver = startBtScanningIfNeeded(btFilters, forceStartScanningAll);
+        // Start Wi-Fi scanning (if needed)
+        mWifiReceiver = startWifiScanningIfNeeded(wifiFilters, forceStartScanningAll);
+        // Start BLE scanning (if needed)
+        mBleScanCallback = startBleScanningIfNeeded(bleFilters, forceStartScanningAll);
+
+        // Schedule a time-out.
+        Handler.getMain().postDelayed(mTimeoutRunnable, SCAN_TIMEOUT);
     }
 
     @MainThread
-    void onDeviceLostMainThread(@Nullable DeviceFilterPair device) {
-        mDevicesFound.remove(device);
-        CompanionDeviceActivity.notifyDevicesChanged();
-    }
+    private void stopDiscoveryAndFinish() {
+        if (DEBUG) Log.i(TAG, "stopDiscovery()");
 
-    void onDeviceSelected(String callingPackage, String deviceAddress) {
-        if (callingPackage == null || deviceAddress == null) {
+        if (!mDiscoveryStarted) {
+            stopSelf();
             return;
         }
-        mServiceCallback.complete(deviceAddress);
-    }
 
-    void onCancel() {
-        if (DEBUG) Log.i(LOG_TAG, "onCancel()");
-        mActivity = null;
-        mServiceCallback.cancel(true);
-    }
+        if (mDiscoveryStopped) return;
+        mDiscoveryStopped = true;
 
-    /**
-     * A pair of device and a filter that matched this device if any.
-     *
-     * @param <T> device type
-     */
-    static class DeviceFilterPair<T extends Parcelable> {
-        public final T device;
-        @Nullable
-        public final DeviceFilter<T> filter;
-
-        private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
-            this.device = device;
-            this.filter = filter;
+        // Stop BT discovery.
+        if (mBtReceiver != null) {
+            // Cancel discovery.
+            mBtAdapter.cancelDiscovery();
+            // Unregister receiver.
+            unregisterReceiver(mBtReceiver);
+            mBtReceiver = null;
         }
 
-        /**
-         * {@code (device, null)} if the filters list is empty or null
-         * {@code null} if none of the provided filters match the device
-         * {@code (device, filter)} where filter is among the list of filters and matches the device
-         */
-        @Nullable
-        public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
-                T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
-            if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
-            final DeviceFilter<T> matchingFilter
-                    = CollectionUtils.find(filters, f -> f.matches(dev));
-
-            DeviceFilterPair<T> result = matchingFilter != null
-                    ? new DeviceFilterPair<>(dev, matchingFilter)
-                    : null;
-            if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters +
-                    ") -> " + result);
-            return result;
+        // Stop Wi-Fi scanning.
+        if (mWifiReceiver != null) {
+            // TODO: need to stop scan?
+            // Unregister receiver.
+            unregisterReceiver(mWifiReceiver);
+            mWifiReceiver = null;
         }
 
-        public String getDisplayName() {
-            if (filter == null) {
-                Preconditions.checkNotNull(device);
-                if (device instanceof BluetoothDevice) {
-                    return getDeviceDisplayNameInternal((BluetoothDevice) device);
-                } else if (device instanceof android.net.wifi.ScanResult) {
-                    return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
-                } else if (device instanceof ScanResult) {
-                    return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
-                } else {
-                    throw new IllegalArgumentException("Unknown device type: " + device.getClass());
-                }
+        // Stop BLE scanning.
+        if (mBleScanCallback != null) {
+            mBleScanner.stopScan(mBleScanCallback);
+        }
+
+        Handler.getMain().removeCallbacks(mTimeoutRunnable);
+
+        // "Finish".
+        stopSelf();
+    }
+
+    private void checkBoundDevicesIfNeeded(@NonNull AssociationRequest request,
+            @NonNull List<BluetoothDeviceFilter> btFilters) {
+        // If filtering to get single device by mac address, also search in the set of already
+        // bonded devices to allow linking those directly
+        if (btFilters.isEmpty() || !request.isSingleDevice()) return;
+
+        final BluetoothDeviceFilter singleMacAddressFilter =
+                find(btFilters, filter -> !TextUtils.isEmpty(filter.getAddress()));
+
+        if (singleMacAddressFilter == null) return;
+
+        findAndReportMatches(mBtAdapter.getBondedDevices(), btFilters);
+        findAndReportMatches(mBtManager.getConnectedDevices(BluetoothProfile.GATT), btFilters);
+        findAndReportMatches(
+                mBtManager.getConnectedDevices(BluetoothProfile.GATT_SERVER), btFilters);
+    }
+
+    private void findAndReportMatches(@Nullable Collection<BluetoothDevice> devices,
+            @NonNull List<BluetoothDeviceFilter> filters) {
+        if (devices == null) return;
+
+        for (BluetoothDevice device : devices) {
+            final DeviceFilterPair<BluetoothDevice> match = findMatch(device, filters);
+            if (match != null) {
+                onDeviceFound(match);
             }
-            return filter.getDeviceDisplayName(device);
+        }
+    }
+
+    private BluetoothBroadcastReceiver startBtScanningIfNeeded(
+            List<BluetoothDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "registerReceiver(BluetoothDevice.ACTION_FOUND)");
+
+        final BluetoothBroadcastReceiver receiver = new BluetoothBroadcastReceiver(filters);
+
+        final IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
+        registerReceiver(receiver, intentFilter);
+
+        mBtAdapter.startDiscovery();
+
+        return receiver;
+    }
+
+    private WifiBroadcastReceiver startWifiScanningIfNeeded(
+            List<WifiDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "registerReceiver(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)");
+
+        final WifiBroadcastReceiver receiver = new WifiBroadcastReceiver(filters);
+
+        final IntentFilter intentFilter = new IntentFilter(
+                WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+        registerReceiver(receiver, intentFilter);
+
+        mWifiManager.startScan();
+
+        return receiver;
+    }
+
+    private ScanCallback startBleScanningIfNeeded(
+            List<BluetoothLeDeviceFilter> filters, boolean force) {
+        if (isEmpty(filters) && !force) return null;
+        if (DEBUG) Log.d(TAG, "BLEScanner.startScan");
+
+        if (mBleScanner == null) {
+            Log.w(TAG, "BLE Scanner is not available.");
+            return null;
         }
 
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
-            return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
-        }
+        final BLEScanCallback callback = new BLEScanCallback(filters);
 
-        @Override
-        public int hashCode() {
-            return Objects.hash(getDeviceMacAddress(device));
-        }
+        final List<ScanFilter> scanFilters = map(
+                filters, BluetoothLeDeviceFilter::getScanFilter);
+        final ScanSettings scanSettings = new ScanSettings.Builder()
+                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+                .build();
+        mBleScanner.startScan(scanFilters, scanSettings, callback);
 
-        @Override
-        public String toString() {
-            return "DeviceFilterPair{"
-                    + "device=" + device + " " + getDisplayName()
-                    + ", filter=" + filter
-                    + '}';
-        }
+        return callback;
+    }
+
+    private void onDeviceFound(@NonNull DeviceFilterPair<?> device) {
+        runOnMainThread(() -> {
+            if (DEBUG) Log.v(TAG, "onDeviceFound() " + device);
+            if (mDevicesFound.contains(device)) {
+                // TODO: update the device instead of ignoring (new found device may contain
+                //  additional/updated info, eg. name of the device).
+                if (DEBUG) {
+                    Log.d(TAG, "onDeviceFound() " + device.toShortString()
+                            + " - Already seen: ignore.");
+                }
+                return;
+            }
+            if (DEBUG) Log.i(TAG, "onDeviceFound() " + device.toShortString() + " - New device.");
+
+            // First: make change.
+            mDevicesFound.add(device);
+            // Then: notify observers.
+            SCAN_RESULTS_OBSERVABLE.notifyObservers();
+        });
+    }
+
+    private void onDeviceLost(@Nullable DeviceFilterPair<?> device) {
+        runOnMainThread(() -> {
+            if (DEBUG) Log.i(TAG, "onDeviceLost(), device=" + device.toShortString());
+
+            // First: make change.
+            mDevicesFound.remove(device);
+            // Then: notify observers.
+            SCAN_RESULTS_OBSERVABLE.notifyObservers();
+        });
+    }
+
+    private void timeout() {
+        if (DEBUG) Log.i(TAG, "timeout()");
+        stopDiscoveryAndFinish();
+        TIMEOUT_OBSERVABLE.notifyObservers();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
     }
 
     private class BLEScanCallback extends ScanCallback {
+        final List<BluetoothLeDeviceFilter> mFilters;
 
-        public BLEScanCallback() {
-            if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
+        BLEScanCallback(List<BluetoothLeDeviceFilter> filters) {
+            mFilters = filters;
         }
 
         @Override
         public void onScanResult(int callbackType, ScanResult result) {
             if (DEBUG) {
-                Log.i(LOG_TAG,
-                        "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result
-                                + ")");
+                Log.v(TAG, "BLE.onScanResult() callback=" + callbackType + ", result=" + result);
             }
-            final DeviceFilterPair<ScanResult> deviceFilterPair
-                    = DeviceFilterPair.findMatch(result, mBLEFilters);
-            if (deviceFilterPair == null) return;
+
+            final DeviceFilterPair<ScanResult> match = findMatch(result, mFilters);
+            if (match == null) return;
+
             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
-                onDeviceLost(deviceFilterPair);
+                onDeviceLost(match);
             } else {
-                onDeviceFound(deviceFilterPair);
+                // TODO: check this logic.
+                onDeviceFound(match);
             }
         }
     }
 
     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
+        final List<BluetoothDeviceFilter> mFilters;
+
+        BluetoothBroadcastReceiver(List<BluetoothDeviceFilter> filters) {
+            this.mFilters = filters;
+        }
+
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (DEBUG) {
-                Log.i(LOG_TAG,
-                        "BL.onReceive(context = " + context + ", intent = " + intent + ")");
-            }
+            final String action = intent.getAction();
             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
-            final DeviceFilterPair<BluetoothDevice> deviceFilterPair
-                    = DeviceFilterPair.findMatch(device, mBluetoothFilters);
-            if (deviceFilterPair == null) return;
-            if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
-                onDeviceFound(deviceFilterPair);
+
+            if (DEBUG) Log.v(TAG, action + ", device=" + device);
+
+            if (action == null) return;
+
+            final DeviceFilterPair<BluetoothDevice> match = findMatch(device, mFilters);
+            if (match == null) return;
+
+            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
+                onDeviceFound(match);
             } else {
-                onDeviceLost(deviceFilterPair);
+                // TODO: check this logic.
+                onDeviceLost(match);
             }
         }
     }
 
     private class WifiBroadcastReceiver extends BroadcastReceiver {
+        final List<WifiDeviceFilter> mFilters;
+
+        private WifiBroadcastReceiver(List<WifiDeviceFilter> filters) {
+            this.mFilters = filters;
+        }
+
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
-                List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
+            if (!Objects.equals(intent.getAction(), WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
+                return;
+            }
 
-                if (DEBUG) {
-                    Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
-                }
+            final List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
+            if (DEBUG) {
+                Log.v(TAG, "WifiManager.SCAN_RESULTS_AVAILABLE_ACTION, results:\n  "
+                        + TextUtils.join("\n  ", scanResults));
+            }
 
-                for (int i = 0; i < scanResults.size(); i++) {
-                    onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters));
+            for (int i = 0; i < scanResults.size(); i++) {
+                final android.net.wifi.ScanResult scanResult = scanResults.get(i);
+                final DeviceFilterPair<?> match = findMatch(scanResult, mFilters);
+                if (match != null) {
+                    onDeviceFound(match);
                 }
             }
         }
     }
+
+    /**
+     * {@code (device, null)} if the filters list is empty or null
+     * {@code null} if none of the provided filters match the device
+     * {@code (device, filter)} where filter is among the list of filters and matches the device
+     */
+    @Nullable
+    public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
+            T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
+        if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
+        final DeviceFilter<T> matchingFilter = find(filters, f -> f.matches(dev));
+
+        DeviceFilterPair<T> result = matchingFilter != null
+                ? new DeviceFilterPair<>(dev, matchingFilter) : null;
+        if (DEBUG) {
+            Log.v(TAG, "findMatch(dev=" + dev + ", filters=" + filters + ") -> " + result);
+        }
+        return result;
+    }
+
+    private static class MyObservable extends Observable {
+        @Override
+        public void notifyObservers() {
+            setChanged();
+            super.notifyObservers();
+        }
+    }
 }
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
new file mode 100644
index 0000000..faca1ae
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
@@ -0,0 +1,108 @@
+/*
+ * 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.companiondevicemanager;
+
+import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
+import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.companion.DeviceFilter;
+import android.net.MacAddress;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A pair of device and a filter that matched this device if any.
+ *
+ * @param <T> device type.
+ */
+class DeviceFilterPair<T extends Parcelable> {
+    private final T mDevice;
+    private final @Nullable DeviceFilter<T> mFilter;
+
+    DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
+        this.mDevice = device;
+        this.mFilter = filter;
+    }
+
+    T getDevice() {
+        return mDevice;
+    }
+
+    String getDisplayName() {
+        if (mFilter != null) mFilter.getDeviceDisplayName(mDevice);
+
+        if (mDevice instanceof BluetoothDevice) {
+            return getDeviceDisplayNameInternal((BluetoothDevice) mDevice);
+        } else if (mDevice instanceof android.bluetooth.le.ScanResult) {
+            final android.bluetooth.le.ScanResult bleScanResult =
+                    (android.bluetooth.le.ScanResult) mDevice;
+            return getDeviceDisplayNameInternal(bleScanResult.getDevice());
+        } else if (mDevice instanceof android.net.wifi.ScanResult) {
+            final android.net.wifi.ScanResult wifiScanResult =
+                    (android.net.wifi.ScanResult) mDevice;
+            return getDeviceDisplayNameInternal(wifiScanResult);
+        } else {
+            throw new IllegalArgumentException("Unknown device type: " + mDevice.getClass());
+        }
+    }
+
+    @NonNull MacAddress getMacAddress() {
+        return MacAddress.fromString(getDeviceMacAddress(getDevice()));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
+        return Objects.equals(getDeviceMacAddress(mDevice), getDeviceMacAddress(that.mDevice));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getDeviceMacAddress(mDevice));
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceFilterPair{"
+                + "device=" + mDevice + " " + getDisplayName()
+                + ", filter=" + mFilter
+                + '}';
+    }
+
+    @NonNull String toShortString() {
+        return '(' + getDeviceTypeAsString() + ") " + getMacAddress() + " '" + getDisplayName()
+                + '\'';
+    }
+
+    private @NonNull String getDeviceTypeAsString() {
+        if (mDevice instanceof BluetoothDevice) {
+            return "BT";
+        } else if (mDevice instanceof android.bluetooth.le.ScanResult) {
+            return "BLE";
+        } else if (mDevice instanceof android.net.wifi.ScanResult) {
+            return "Wi-Fi";
+        } else {
+            return "Unknown";
+        }
+    }
+}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java
new file mode 100644
index 0000000..cf2a2bf
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * 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.companiondevicemanager;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
+
+/**
+ * Adapter for the list of "found" devices.
+ */
+class DeviceListAdapter extends BaseAdapter implements Observer {
+    private final Context mContext;
+    private final Resources mResources;
+
+    private final Drawable mBluetoothIcon;
+    private final Drawable mWifiIcon;
+
+    private final @ColorInt int mTextColor;
+
+    // List if pairs (display name, address)
+    private List<DeviceFilterPair<?>> mDevices;
+
+    DeviceListAdapter(Context context) {
+        mContext = context;
+        mResources = context.getResources();
+        mBluetoothIcon = getTintedIcon(mResources, android.R.drawable.stat_sys_data_bluetooth);
+        mWifiIcon = getTintedIcon(mResources, com.android.internal.R.drawable.ic_wifi_signal_3);
+        mTextColor = getColor(context, android.R.attr.colorForeground);
+    }
+
+    @Override
+    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+        final TextView view = convertView != null ? (TextView) convertView : newView();
+        bind(view, getItem(position));
+        return view;
+    }
+
+    private void bind(TextView textView, DeviceFilterPair<?> item) {
+        textView.setText(item.getDisplayName());
+        textView.setBackgroundColor(Color.TRANSPARENT);
+        /*
+        textView.setCompoundDrawablesWithIntrinsicBounds(
+                item.getDevice() instanceof android.net.wifi.ScanResult
+                        ? mWifiIcon
+                        : mBluetoothIcon,
+                null, null, null);
+        textView.getCompoundDrawables()[0].setTint(mTextColor);
+         */
+    }
+
+    private TextView newView() {
+        final TextView textView = new TextView(mContext);
+        textView.setTextColor(mTextColor);
+        final int padding = 24;
+        textView.setPadding(padding, padding, padding, padding);
+        //textView.setCompoundDrawablePadding(padding);
+        return textView;
+    }
+
+    @Override
+    public int getCount() {
+        return mDevices != null ? mDevices.size() : 0;
+    }
+
+    @Override
+    public DeviceFilterPair<?> getItem(int position) {
+        return mDevices.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public void update(Observable o, Object arg) {
+        mDevices = CompanionDeviceDiscoveryService.getScanResults();
+        notifyDataSetChanged();
+    }
+
+    private @ColorInt int getColor(Context context, int attr) {
+        final TypedArray a = context.obtainStyledAttributes(new TypedValue().data,
+                new int[] { attr });
+        final int color = a.getColor(0, 0);
+        a.recycle();
+        return color;
+    }
+
+    private static Drawable getTintedIcon(Resources resources, int drawableRes) {
+        Drawable icon = resources.getDrawable(drawableRes, null);
+        icon.setTint(Color.DKGRAY);
+        return icon;
+    }
+}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
new file mode 100644
index 0000000..eab421e
--- /dev/null
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java
@@ -0,0 +1,84 @@
+/*
+ * 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.companiondevicemanager;
+
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.text.Html;
+import android.text.Spanned;
+
+/**
+ * Utilities.
+ */
+class Utils {
+
+    /**
+     * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+     * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+     * unmarshall.
+     */
+    static <T extends ResultReceiver> ResultReceiver prepareResultReceiverForIpc(T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
+    }
+
+    static @NonNull CharSequence getApplicationLabel(
+            @NonNull Context context, @NonNull String packageName) {
+        final PackageManager packageManager = context.getPackageManager();
+        final ApplicationInfo appInfo;
+        try {
+            appInfo = packageManager.getApplicationInfo(packageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        return packageManager.getApplicationLabel(appInfo);
+    }
+
+    static Spanned getHtmlFromResources(
+            @NonNull Context context, @StringRes int resId, CharSequence... formatArgs) {
+        final String[] escapedArgs = new String[formatArgs.length];
+        for (int i = 0; i < escapedArgs.length; i++) {
+            escapedArgs[i] = Html.escapeHtml(formatArgs[i]);
+        }
+        final String plain = context.getString(resId, (Object[]) escapedArgs);
+        return Html.fromHtml(plain, 0);
+    }
+
+    static void runOnMainThread(Runnable runnable) {
+        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
+            runnable.run();
+        } else {
+            Handler.getMain().post(runnable);
+        }
+    }
+
+    private Utils() {
+    }
+}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 23c53a1..f60bc97 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -165,6 +165,7 @@
     <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" />
     <uses-permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE" />
     <uses-permission android:name="com.android.permission.USE_SYSTEM_DATA_LOADERS" />
+    <uses-permission android:name="android.permission.PACKAGE_VERIFICATION_AGENT" />
     <uses-permission android:name="android.permission.MOVE_PACKAGE" />
     <uses-permission android:name="android.permission.KEEP_UNINSTALLED_PACKAGES" />
     <uses-permission android:name="android.permission.CLEAR_APP_USER_DATA" />
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index a4aeae9..875b7e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -410,7 +410,7 @@
      * dragging it and translation should be deferred {@see KeyguardBouncer#show(boolean, boolean)}
      */
     public void showGenericBouncer(boolean scrimmed) {
-        if (mAlternateAuthInterceptor != null) {
+        if (shouldShowAltAuth()) {
             updateAlternateAuthShowing(mAlternateAuthInterceptor.showAlternateAuthBouncer());
             return;
         }
@@ -418,6 +418,11 @@
         showBouncer(scrimmed);
     }
 
+    private boolean shouldShowAltAuth() {
+        return mAlternateAuthInterceptor != null
+                && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true);
+    }
+
     /**
      * Hides the input bouncer (pin/password/pattern).
      */
@@ -473,7 +478,7 @@
 
             // If there is an an alternate auth interceptor (like the UDFPS), show that one instead
             // of the bouncer.
-            if (mAlternateAuthInterceptor != null) {
+            if (shouldShowAltAuth()) {
                 if (!afterKeyguardGone) {
                     mBouncer.setDismissAction(mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
                     mAfterKeyguardGoneAction = null;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index cbaa460..5d80bca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -375,6 +375,39 @@
     }
 
     @Test
+    public void testShowAltAuth_unlockingWithBiometricNotAllowed() {
+        // GIVEN alt auth exists, unlocking with biometric isn't allowed
+        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
+        when(mBouncer.isShowing()).thenReturn(false);
+        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(false);
+
+        // WHEN showGenericBouncer is called
+        final boolean scrimmed = true;
+        mStatusBarKeyguardViewManager.showGenericBouncer(scrimmed);
+
+        // THEN regular bouncer is shown
+        verify(mBouncer).show(anyBoolean(), eq(scrimmed));
+        verify(mAlternateAuthInterceptor, never()).showAlternateAuthBouncer();
+    }
+
+    @Test
+    public void testShowAltAuth_unlockingWithBiometricAllowed() {
+        // GIVEN alt auth exists, unlocking with biometric is allowed
+        mStatusBarKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
+        when(mBouncer.isShowing()).thenReturn(false);
+        when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+                .thenReturn(true);
+
+        // WHEN showGenericBouncer is called
+        mStatusBarKeyguardViewManager.showGenericBouncer(true);
+
+        // THEN alt auth bouncer is shown
+        verify(mAlternateAuthInterceptor).showAlternateAuthBouncer();
+        verify(mBouncer, never()).show(anyBoolean(), anyBoolean());
+    }
+
+    @Test
     public void testUpdateResources_delegatesToBouncer() {
         mStatusBarKeyguardViewManager.updateResources();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 71ae5eb..c7943c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -116,7 +116,6 @@
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.BubbleIconFactory;
 import com.android.wm.shell.bubbles.BubbleLogger;
-import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.bubbles.BubbleViewInfoTask;
 import com.android.wm.shell.bubbles.Bubbles;
@@ -631,7 +630,7 @@
     }
 
     @Test
-    public void testRemoveLastExpanded_selectsOverflow() {
+    public void testRemoveLastExpanded_collapses() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onPendingEntryAdded(mRow);
         mEntryListener.onPendingEntryAdded(mRow2);
@@ -670,11 +669,10 @@
                         stackView.getExpandedBubble().getKey()).getKey(),
                 Bubbles.DISMISS_USER_GESTURE);
 
-        // Overflow should be selected
-        assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
-        verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
-        assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        // We should be collapsed
+        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
+        assertFalse(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 7b9e6a9..65c219c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -19,8 +19,11 @@
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -97,7 +100,6 @@
 import com.android.wm.shell.bubbles.BubbleDataRepository;
 import com.android.wm.shell.bubbles.BubbleEntry;
 import com.android.wm.shell.bubbles.BubbleLogger;
-import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.common.DisplayController;
@@ -572,7 +574,7 @@
     }
 
     @Test
-    public void testRemoveLastExpanded_selectsOverflow() {
+    public void testRemoveLastExpanded_collapses() {
         // Mark it as a bubble and add it explicitly
         mEntryListener.onEntryAdded(mRow);
         mEntryListener.onEntryAdded(mRow2);
@@ -611,11 +613,10 @@
                         stackView.getExpandedBubble().getKey()).getKey(),
                 Bubbles.DISMISS_USER_GESTURE);
 
-        // Overflow should be selected
-        assertEquals(mBubbleData.getSelectedBubble().getKey(), BubbleOverflow.KEY);
-        verify(mBubbleExpandListener).onBubbleExpandChanged(true, BubbleOverflow.KEY);
-        assertTrue(mBubbleController.hasBubbles());
-        assertSysuiStates(true /* stackExpanded */, false /* mangeMenuExpanded */);
+        // We should be collapsed
+        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getKey());
+        assertFalse(mBubbleController.hasBubbles());
+        assertSysuiStates(false /* stackExpanded */, false /* mangeMenuExpanded */);
     }
 
     @Test
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index bcc345f..637994f 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -16,8 +16,13 @@
 
 package com.android.server.companion;
 
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
+import static android.content.ComponentName.createRelative;
+
 import static com.android.internal.util.CollectionUtils.filter;
-import static com.android.internal.util.FunctionalUtils.uncheckExceptions;
 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
 import static com.android.server.companion.CompanionDeviceManagerService.LOG_TAG;
 import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
@@ -28,73 +33,114 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.PendingIntent;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
-import android.companion.CompanionDeviceManager;
 import android.companion.IAssociationRequestCallback;
-import android.companion.ICompanionDeviceDiscoveryService;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.Signature;
+import android.net.MacAddress;
 import android.os.Binder;
-import android.os.IBinder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 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.Objects;
 import java.util.Set;
 
+/**
+ * Class responsible for handling incoming {@link AssociationRequest}s.
+ * The main responsibilities of an {@link AssociationRequestsProcessor} are:
+ * <ul>
+ * <li> Requests validation and checking if the package that would own the association holds all
+ * necessary permissions.
+ * <li> Communication with the requester via a provided
+ * {@link android.companion.CompanionDeviceManager.Callback}.
+ * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
+ * approval.
+ * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
+ * request was found valid and was approved by user.
+ * </ul>
+ *
+ * The class supports two variants of the "Association Flow": the full variant, and the shortened
+ * (a.k.a. No-UI) variant.
+ * Both flows start similarly: in
+ * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
+ * invoked from
+ * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
+ * method call.
+ * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
+ * required.
+ *
+ * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}
+ * which after calling to  {@link CompanionDeviceManagerService} to create an association, notifies
+ * the requester via
+ * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
+ *
+ * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
+ * {@link PendingIntent} for the approval UI and sends it back to the requester via
+ * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
+ * When/if user approves the request,  {@link AssociationRequestsProcessor} receives a "callback"
+ * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
+ * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
+ * which one more time checks that the packages holds all necessary permissions before proceeding to
+ * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}.
+ *
+ * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
+ * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
+ * ResultReceiver, MacAddress)
+ */
 class AssociationRequestsProcessor {
     private static final String TAG = LOG_TAG + ".AssociationRequestsProcessor";
 
-    private static final ComponentName SERVICE_TO_BIND_TO = ComponentName.createRelative(
-            CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
-            ".CompanionDeviceDiscoveryService");
+    private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
+            createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
+
+    // AssociationRequestsProcessor <-> UI
+    private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
+    private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
+    private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
+
+    // AssociationRequestsProcessor -> UI
+    private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
+    private static final String EXTRA_ASSOCIATION = "association";
+
+    // UI -> AssociationRequestsProcessor
+    private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
+    private static final String EXTRA_MAC_ADDRESS = "mac_address";
 
     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 final PerUser<ServiceConnector<ICompanionDeviceDiscoveryService>> mServiceConnectors;
-
-    private AssociationRequest mRequest;
-    private IAssociationRequestCallback mAppCallback;
-    private AndroidFuture<?> mOngoingDeviceDiscovery;
+    private final PackageManagerInternal mPackageManager;
 
     AssociationRequestsProcessor(CompanionDeviceManagerService service) {
         mContext = service.getContext();
         mService = service;
-
-        final Intent serviceIntent = new Intent().setComponent(SERVICE_TO_BIND_TO);
-        mServiceConnectors = new PerUser<>() {
-            @Override
-            protected ServiceConnector<ICompanionDeviceDiscoveryService> create(int userId) {
-                return new ServiceConnector.Impl<>(
-                    mContext,
-                    serviceIntent, 0/* bindingFlags */, userId,
-                    ICompanionDeviceDiscoveryService.Stub::asInterface);
-            }
-        };
+        mPackageManager = service.mPackageManagerInternal;
     }
 
     /**
      * Handle incoming {@link AssociationRequest}s, sent via
      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
      */
-    void process(@NonNull AssociationRequest request, @NonNull String packageName,
-            @UserIdInt int userId, @NonNull IAssociationRequestCallback callback) {
+    void processNewAssociationRequest(@NonNull AssociationRequest request,
+            @NonNull String packageName, @UserIdInt int userId,
+            @NonNull IAssociationRequestCallback callback) {
         requireNonNull(request, "Request MUST NOT be null");
         if (request.isSelfManaged()) {
             requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
@@ -103,14 +149,15 @@
         requireNonNull(packageName, "Package name MUST NOT be null");
         requireNonNull(callback, "Callback MUST NOT be null");
 
+        final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
         if (DEBUG) {
-            Slog.d(TAG, "process() "
+            Slog.d(TAG, "processNewAssociationRequest() "
                     + "request=" + request + ", "
-                    + "package=u" + userId + "/" + packageName);
+                    + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
         }
 
         // 1. Enforce permissions and other requirements.
-        enforcePermissionsForAssociation(mContext, request, packageName, userId);
+        enforcePermissionsForAssociation(mContext, request, packageUid);
         mService.checkUsesFeature(packageName, userId);
 
         // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
@@ -118,71 +165,99 @@
         if (request.isSelfManaged() && !request.isForceConfirmation()
                 && !willAddRoleHolder(request, packageName, userId)) {
             // 2a. Create association right away.
-            final AssociationInfo association = mService.createAssociation(userId, packageName,
-                /* macAddress */ null, request.getDisplayName(), request.getDeviceProfile(),
-                /* selfManaged */true);
-            withCatchingRemoteException(() -> callback.onAssociationCreated(association));
+            createAssociationAndNotifyApplication(request, packageName, userId,
+                    /*macAddress*/ null, callback);
             return;
         }
 
-        // 2b. Launch the UI.
-        synchronized (mService.mLock) {
-            if (mRequest != null) {
-                Slog.w(TAG, "CDM is already processing another AssociationRequest.");
+        // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
 
-                withCatchingRemoteException(() -> callback.onFailure("Busy."));
-            }
+        // 2b.1. Populate the request with required info.
+        request.setPackageName(packageName);
+        request.setUserId(userId);
+        request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId));
 
-            final boolean linked = withCatchingRemoteException(
-                    () -> callback.asBinder().linkToDeath(mBinderDeathRecipient, 0));
-            if (!linked) {
-                // The process has died by now: do not proceed.
-                return;
-            }
+        // 2b.2. Prepare extras and create an Intent.
+        final Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
+        extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
+        extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
 
-            mRequest = request;
+        final Intent intent = new Intent();
+        intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+        intent.putExtras(extras);
+
+        // 2b.3. Create a PendingIntent.
+        final PendingIntent pendingIntent;
+        final long token = Binder.clearCallingIdentity();
+        try {
+            // 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.
+            pendingIntent = PendingIntent.getActivity(mContext, /*requestCode */ packageUid, intent,
+                    FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
 
-        mAppCallback = callback;
-        request.setCallingPackage(packageName);
+        // 2b.4. Send the PendingIntent back to the app.
+        try {
+            callback.onAssociationPending(pendingIntent);
+        } catch (RemoteException ignore) { }
+    }
 
-        if (mayAssociateWithoutPrompt(packageName, userId)) {
-            Slog.i(TAG, "setSkipPrompt(true)");
-            request.setSkipPrompt(true);
+    private void processAssociationRequestApproval(@NonNull AssociationRequest request,
+            @NonNull IAssociationRequestCallback callback,
+            @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
+        final String packageName = request.getPackageName();
+        final int userId = request.getUserId();
+        final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
+
+        if (DEBUG) {
+            Slog.d(TAG, "processAssociationRequestApproval()\n"
+                    + "   package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
+                    + "   request=" + request + "\n"
+                    + "   macAddress=" + macAddress + "\n");
         }
 
-        final String deviceProfile = request.getDeviceProfile();
-        mOngoingDeviceDiscovery = getDeviceProfilePermissionDescription(deviceProfile)
-                .thenComposeAsync(description -> {
-                    if (DEBUG) {
-                        Slog.d(TAG, "fetchProfileDescription done: " + description);
-                    }
+        // 1. Need to check permissions again in case something changed, since we first received
+        // this request.
+        try {
+            enforcePermissionsForAssociation(mContext, request, packageUid);
+        } catch (SecurityException e) {
+            // Since, at this point the caller is our own UI, we need to catch the exception on
+            // forward it back to the application via the callback.
+            try {
+                callback.onFailure(e.getMessage());
+            } catch (RemoteException ignore) { }
+            return;
+        }
 
-                    request.setDeviceProfilePrivilegesDescription(description);
+        // 2. Create association and notify the application.
+        final AssociationInfo association = createAssociationAndNotifyApplication(
+                request, packageName, userId, macAddress, callback);
 
-                    return mServiceConnectors.forUser(userId).postAsync(service -> {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Connected to CDM service -> "
-                                    + "Starting discovery for " + request);
-                        }
+        // 3. Send the association back the Approval Activity, so that it can report back to the app
+        // via Activity.setResult().
+        final Bundle data = new Bundle();
+        data.putParcelable(EXTRA_ASSOCIATION, association);
+        resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
+    }
 
-                        AndroidFuture<String> future = new AndroidFuture<>();
-                        service.startDiscovery(request, packageName, callback, future);
-                        return future;
-                    }).cancelTimeout();
+    private AssociationInfo createAssociationAndNotifyApplication(
+            @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
+            @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) {
+        final AssociationInfo association = mService.createAssociation(userId, packageName,
+                macAddress, request.getDisplayName(), request.getDeviceProfile(),
+                request.isSelfManaged());
 
-                }, FgThread.getExecutor()).whenComplete(uncheckExceptions((deviceAddress, err) -> {
-                    if (err == null) {
-                        mService.legacyCreateAssociation(
-                                userId, deviceAddress, packageName, deviceProfile);
-                        mServiceConnectors.forUser(userId).post(
-                                ICompanionDeviceDiscoveryService::onAssociationCreated);
-                    } else {
-                        Slog.e(TAG, "Failed to discover device(s)", err);
-                        callback.onFailure("No devices found: " + err.getMessage());
-                    }
-                    cleanup();
-                }));
+        try {
+            callback.onAssociationCreated(association);
+        } catch (RemoteException ignore) { }
+
+        return association;
     }
 
     private boolean willAddRoleHolder(@NonNull AssociationRequest request,
@@ -197,26 +272,44 @@
         return !isRoleHolder;
     }
 
-    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);
+    private final ResultReceiver mOnRequestConfirmationReceiver =
+            new ResultReceiver(Handler.getMain()) {
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle data) {
+            if (DEBUG) {
+                Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
+                        + "code=" + resultCode + ", " + "data=" + data);
             }
-            if (mAppCallback != null) {
-                mAppCallback.asBinder().unlinkToDeath(mBinderDeathRecipient, 0);
-                mAppCallback = null;
-            }
-            mRequest = null;
-        }
-    }
 
-    private boolean mayAssociateWithoutPrompt(String packageName, int userId) {
-        final String deviceProfile = mRequest.getDeviceProfile();
+            if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
+                Slog.w(TAG, "Unknown result code:" + resultCode);
+                return;
+            }
+
+            final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST);
+            final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
+                    .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
+            final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER);
+
+            requireNonNull(request);
+            requireNonNull(callback);
+            requireNonNull(resultReceiver);
+
+            final MacAddress macAddress;
+            if (request.isSelfManaged()) {
+                macAddress = null;
+            } else {
+                macAddress = data.getParcelable(EXTRA_MAC_ADDRESS);
+                requireNonNull(macAddress);
+            }
+
+            processAssociationRequestApproval(request, callback, resultReceiver, macAddress);
+        }
+    };
+
+    private boolean mayAssociateWithoutPrompt(@NonNull AssociationRequest request,
+            @NonNull String packageName, @UserIdInt int userId) {
+        final String deviceProfile = request.getDeviceProfile();
         if (deviceProfile != null) {
             final boolean isRoleHolder = Binder.withCleanCallingIdentity(
                     () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
@@ -252,8 +345,8 @@
         String[] sameOemCerts = mContext.getResources()
                 .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
 
-        Signature[] signatures = mService.mPackageManagerInternal
-                .getPackage(packageName).getSigningDetails().getSignatures();
+        Signature[] signatures = mPackageManager.getPackage(packageName).getSigningDetails()
+                .getSignatures();
         String[] apkCerts = PackageUtils.computeSignaturesSha256Digests(signatures);
 
         Set<String> sameOemPackageCerts =
@@ -274,47 +367,6 @@
         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<>();
@@ -330,16 +382,19 @@
         return sameOemPackageCerts;
     }
 
-    private static boolean withCatchingRemoteException(ThrowingRunnable runnable) {
-        try {
-            runnable.run();
-        } catch (RemoteException e) {
-            return false;
-        }
-        return true;
-    }
+    /**
+     * Convert an instance of a "locally-defined" ResultReceiver to an instance of
+     * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
+     * unmarshall.
+     */
+    private static <T extends ResultReceiver> ResultReceiver prepareForIpc(T resultReceiver) {
+        final Parcel parcel = Parcel.obtain();
+        resultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
 
-    private interface ThrowingRunnable {
-        void run() throws RemoteException;
+        final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        return ipcFriendly;
     }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 9c996f4..626128a 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -406,7 +406,8 @@
             enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
                     "create associations");
 
-            mAssociationRequestsProcessor.process(request, packageName, userId, callback);
+            mAssociationRequestsProcessor.processNewAssociationRequest(
+                    request, packageName, userId, callback);
         }
 
         @Override
@@ -717,8 +718,6 @@
                         .append(sDateFormat.format(time)).append('\n');
             }
 
-            mAssociationRequestsProcessor.dump(fout);
-
             fout.append("Device Listener Services State:").append('\n');
             for (int i = 0, size =  mCompanionDevicePresenceController.mBoundServices.size();
                     i < size; i++) {
diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java
index ea57089..3a8ee73 100644
--- a/services/companion/java/com/android/server/companion/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java
@@ -38,13 +38,11 @@
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
-import android.content.pm.PackageManagerInternal;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 
 import com.android.internal.app.IAppOpsService;
-import com.android.server.LocalServices;
 
 import java.util.Map;
 
@@ -69,9 +67,7 @@
     }
 
     static void enforcePermissionsForAssociation(@NonNull Context context,
-            @NonNull AssociationRequest request, @NonNull String packageName,
-            @UserIdInt int userId) {
-        final int packageUid = getPackageUid(userId, packageName);
+            @NonNull AssociationRequest request, int packageUid) {
         enforceRequestDeviceProfilePermissions(context, request.getDeviceProfile(), packageUid);
 
         if (request.isSelfManaged()) {
@@ -207,11 +203,6 @@
         }
     }
 
-    private static int getPackageUid(@UserIdInt int userId, @NonNull String packageName) {
-        return LocalServices.getService(PackageManagerInternal.class)
-                .getPackageUid(packageName, 0, userId);
-    }
-
     private static IAppOpsService getAppOpsService() {
         if (sAppOpsService == null) {
             synchronized (PermissionsUtils.class) {
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 2550e3a..ffef803 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -112,7 +112,6 @@
 import com.android.server.location.injector.DeviceStationaryHelper;
 import com.android.server.location.injector.EmergencyHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationPermissionsHelper;
 import com.android.server.location.injector.LocationPowerSaveModeHelper;
 import com.android.server.location.injector.LocationUsageLogger;
@@ -1705,7 +1704,6 @@
         private final SystemScreenInteractiveHelper mScreenInteractiveHelper;
         private final SystemDeviceStationaryHelper mDeviceStationaryHelper;
         private final SystemDeviceIdleHelper mDeviceIdleHelper;
-        private final LocationAttributionHelper mLocationAttributionHelper;
         private final LocationUsageLogger mLocationUsageLogger;
 
         // lazily instantiated since they may not always be used
@@ -1731,7 +1729,6 @@
             mScreenInteractiveHelper = new SystemScreenInteractiveHelper(context);
             mDeviceStationaryHelper = new SystemDeviceStationaryHelper();
             mDeviceIdleHelper = new SystemDeviceIdleHelper(context);
-            mLocationAttributionHelper = new LocationAttributionHelper(mAppOpsHelper);
             mLocationUsageLogger = new LocationUsageLogger();
         }
 
@@ -1808,11 +1805,6 @@
         }
 
         @Override
-        public LocationAttributionHelper getLocationAttributionHelper() {
-            return mLocationAttributionHelper;
-        }
-
-        @Override
         public synchronized EmergencyHelper getEmergencyHelper() {
             if (mEmergencyCallHelper == null) {
                 mEmergencyCallHelper = new SystemEmergencyHelper(mContext);
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index 1781588..699f143 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.server.location.gnss;
 
+import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
+
 import static com.android.server.location.gnss.GnssManagerService.D;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
@@ -32,7 +34,6 @@
 import com.android.server.location.gnss.hal.GnssNative;
 import com.android.server.location.injector.AppOpsHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationUsageLogger;
 import com.android.server.location.injector.SettingsHelper;
 
@@ -68,25 +69,23 @@
         @Nullable
         @Override
         protected void onActive() {
-            mLocationAttributionHelper.reportHighPowerLocationStart(getIdentity());
+            mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
         }
 
         @Nullable
         @Override
         protected void onInactive() {
-            mLocationAttributionHelper.reportHighPowerLocationStop(getIdentity());
+            mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
         }
     }
 
     private final AppOpsHelper mAppOpsHelper;
-    private final LocationAttributionHelper mLocationAttributionHelper;
     private final LocationUsageLogger mLogger;
     private final GnssNative mGnssNative;
 
     public GnssMeasurementsProvider(Injector injector, GnssNative gnssNative) {
         super(injector);
         mAppOpsHelper = injector.getAppOpsHelper();
-        mLocationAttributionHelper = injector.getLocationAttributionHelper();
         mLogger = injector.getLocationUsageLogger();
         mGnssNative = gnssNative;
 
diff --git a/services/core/java/com/android/server/location/injector/Injector.java b/services/core/java/com/android/server/location/injector/Injector.java
index 173fd13..c0ce3a6 100644
--- a/services/core/java/com/android/server/location/injector/Injector.java
+++ b/services/core/java/com/android/server/location/injector/Injector.java
@@ -58,9 +58,6 @@
     /** Returns a DeviceIdleHelper. */
     DeviceIdleHelper getDeviceIdleHelper();
 
-    /** Returns a LocationAttributionHelper. */
-    LocationAttributionHelper getLocationAttributionHelper();
-
     /** Returns an EmergencyHelper. */
     EmergencyHelper getEmergencyHelper();
 
diff --git a/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java b/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java
deleted file mode 100644
index 4838752..0000000
--- a/services/core/java/com/android/server/location/injector/LocationAttributionHelper.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2020 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.location.injector;
-
-import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
-import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
-
-import static com.android.server.location.LocationManagerService.D;
-import static com.android.server.location.LocationManagerService.TAG;
-
-import android.location.util.identity.CallerIdentity;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.Map;
-
-/**
- * Helps manage appop monitoring for multiple location clients.
- */
-public class LocationAttributionHelper {
-
-    private final AppOpsHelper mAppOpsHelper;
-
-    @GuardedBy("this")
-    private final Map<CallerIdentity, Integer> mAttributions;
-    @GuardedBy("this")
-    private final Map<CallerIdentity, Integer> mHighPowerAttributions;
-
-    public LocationAttributionHelper(AppOpsHelper appOpsHelper) {
-        mAppOpsHelper = appOpsHelper;
-
-        mAttributions = new ArrayMap<>();
-        mHighPowerAttributions = new ArrayMap<>();
-    }
-
-    /**
-     * Report normal location usage for the given caller in the given bucket, with a unique key.
-     */
-    public synchronized void reportLocationStart(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mAttributions.getOrDefault(identity, 0);
-        if (count == 0) {
-            if (mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, identity)) {
-                mAttributions.put(identity, 1);
-            }
-        } else {
-            mAttributions.put(identity, count + 1);
-        }
-    }
-
-    /**
-     * Report normal location usage has stopped for the given caller in the given bucket, with a
-     * unique key.
-     */
-    public synchronized void reportLocationStop(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mAttributions.getOrDefault(identity, 0);
-        if (count == 1) {
-            mAttributions.remove(identity);
-            mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, identity);
-        } else if (count > 1) {
-            mAttributions.put(identity, count - 1);
-        }
-    }
-
-    /**
-     * Report high power location usage for the given caller in the given bucket, with a unique
-     * key.
-     */
-    public synchronized void reportHighPowerLocationStart(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mHighPowerAttributions.getOrDefault(identity, 0);
-        if (count == 0) {
-            if (D) {
-                Log.v(TAG, "starting high power location attribution for " + identity);
-            }
-            if (mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, identity)) {
-                mHighPowerAttributions.put(identity, 1);
-            }
-        } else {
-            mHighPowerAttributions.put(identity, count + 1);
-        }
-    }
-
-    /**
-     * Report high power location usage has stopped for the given caller in the given bucket,
-     * with a unique key.
-     */
-    public synchronized void reportHighPowerLocationStop(CallerIdentity identity) {
-        identity = CallerIdentity.forAggregation(identity);
-
-        int count = mHighPowerAttributions.getOrDefault(identity, 0);
-        if (count == 1) {
-            if (D) {
-                Log.v(TAG, "stopping high power location attribution for " + identity);
-            }
-            mHighPowerAttributions.remove(identity);
-            mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, identity);
-        } else if (count > 1) {
-            mHighPowerAttributions.put(identity, count - 1);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index ede8b32..c1d8e78 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.location.provider;
 
+import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
+import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
 import static android.app.compat.CompatChanges.isChangeEnabled;
 import static android.location.LocationManager.DELIVER_HISTORICAL_LOCATIONS;
 import static android.location.LocationManager.GPS_PROVIDER;
@@ -32,6 +34,7 @@
 import static android.os.PowerManager.LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF;
 import static android.os.PowerManager.LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.LocationManagerService.D;
 import static com.android.server.location.LocationManagerService.TAG;
 import static com.android.server.location.LocationPermissions.PERMISSION_COARSE;
@@ -98,7 +101,6 @@
 import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener;
 import com.android.server.location.injector.AppOpsHelper;
 import com.android.server.location.injector.Injector;
-import com.android.server.location.injector.LocationAttributionHelper;
 import com.android.server.location.injector.LocationPermissionsHelper;
 import com.android.server.location.injector.LocationPermissionsHelper.LocationPermissionsListener;
 import com.android.server.location.injector.LocationPowerSaveModeHelper;
@@ -191,7 +193,7 @@
 
         private final ILocationListener mListener;
 
-        protected LocationListenerTransport(ILocationListener listener) {
+        LocationListenerTransport(ILocationListener listener) {
             mListener = Objects.requireNonNull(listener);
         }
 
@@ -300,7 +302,7 @@
 
         private final ILocationCallback mCallback;
 
-        protected GetCurrentLocationTransport(ILocationCallback callback) {
+        GetCurrentLocationTransport(ILocationCallback callback) {
             mCallback = Objects.requireNonNull(callback);
         }
 
@@ -411,7 +413,7 @@
             EVENT_LOG.logProviderClientActive(mName, getIdentity());
 
             if (!getRequest().isHiddenFromAppOps()) {
-                mLocationAttributionHelper.reportLocationStart(getIdentity());
+                mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, getIdentity());
             }
             onHighPowerUsageChanged();
 
@@ -426,7 +428,7 @@
 
             onHighPowerUsageChanged();
             if (!getRequest().isHiddenFromAppOps()) {
-                mLocationAttributionHelper.reportLocationStop(getIdentity());
+                mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, getIdentity());
             }
 
             onProviderListenerInactive();
@@ -500,11 +502,9 @@
 
                 if (!getRequest().isHiddenFromAppOps()) {
                     if (mIsUsingHighPower) {
-                        mLocationAttributionHelper.reportHighPowerLocationStart(
-                                getIdentity());
+                        mAppOpsHelper.startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
                     } else {
-                        mLocationAttributionHelper.reportHighPowerLocationStop(
-                                getIdentity());
+                        mAppOpsHelper.finishOp(OP_MONITOR_HIGH_POWER_LOCATION, getIdentity());
                     }
                 }
             }
@@ -1015,7 +1015,7 @@
     protected final class LocationListenerRegistration extends LocationRegistration implements
             IBinder.DeathRecipient {
 
-        protected LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
+        LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
                 LocationListenerTransport transport, @PermissionLevel int permissionLevel) {
             super(request, identity, transport, permissionLevel);
         }
@@ -1080,7 +1080,7 @@
     protected final class LocationPendingIntentRegistration extends LocationRegistration implements
             PendingIntent.CancelListener {
 
-        protected LocationPendingIntentRegistration(LocationRequest request,
+        LocationPendingIntentRegistration(LocationRequest request,
                 CallerIdentity identity, LocationPendingIntentTransport transport,
                 @PermissionLevel int permissionLevel) {
             super(request, identity, transport, permissionLevel);
@@ -1089,13 +1089,15 @@
         @GuardedBy("mLock")
         @Override
         protected void onLocationListenerRegister() {
-            ((PendingIntent) getKey()).registerCancelListener(this);
+            if (!((PendingIntent) getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
+                remove();
+            }
         }
 
         @GuardedBy("mLock")
         @Override
         protected void onLocationListenerUnregister() {
-            ((PendingIntent) getKey()).unregisterCancelListener(this);
+            ((PendingIntent) getKey()).removeCancelListener(this);
         }
 
         @Override
@@ -1138,7 +1140,7 @@
 
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
-        protected GetCurrentLocationListenerRegistration(LocationRequest request,
+        GetCurrentLocationListenerRegistration(LocationRequest request,
                 CallerIdentity identity, LocationTransport transport, int permissionLevel) {
             super(request, identity, transport, permissionLevel);
         }
@@ -1347,7 +1349,6 @@
     protected final AppForegroundHelper mAppForegroundHelper;
     protected final LocationPowerSaveModeHelper mLocationPowerSaveModeHelper;
     protected final ScreenInteractiveHelper mScreenInteractiveHelper;
-    protected final LocationAttributionHelper mLocationAttributionHelper;
     protected final LocationUsageLogger mLocationUsageLogger;
     protected final LocationFudger mLocationFudger;
 
@@ -1415,7 +1416,6 @@
         mAppForegroundHelper = injector.getAppForegroundHelper();
         mLocationPowerSaveModeHelper = injector.getLocationPowerSaveModeHelper();
         mScreenInteractiveHelper = injector.getScreenInteractiveHelper();
-        mLocationAttributionHelper = injector.getLocationAttributionHelper();
         mLocationUsageLogger = injector.getLocationUsageLogger();
         mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM());
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index be01646..173545c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4441,6 +4441,10 @@
     }
 
     private void updateFontScaleIfNeeded(@UserIdInt int userId) {
+        if (userId != getCurrentUserId()) {
+            return;
+        }
+
         final float scaleFactor = Settings.System.getFloatForUser(mContext.getContentResolver(),
                 FONT_SCALE, 1.0f, userId);
 
@@ -4457,6 +4461,10 @@
     }
 
     private void updateFontWeightAdjustmentIfNeeded(@UserIdInt int userId) {
+        if (userId != getCurrentUserId()) {
+            return;
+        }
+
         final int fontWeightAdjustment =
                 Settings.Secure.getIntForUser(
                         mContext.getContentResolver(),
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
index e633850..005d3e8 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProcessTest.kt
@@ -18,6 +18,7 @@
 
 import android.content.pm.parsing.component.ParsedProcess
 import android.content.pm.parsing.component.ParsedProcessImpl
+import android.util.ArrayMap
 import kotlin.contracts.ExperimentalContracts
 
 @ExperimentalContracts
@@ -29,6 +30,8 @@
     override val excludedMethods = listOf(
         // Copying method
         "addStateFrom",
+        // Utility method
+        "putAppClassNameForPackage",
     )
 
     override val baseParams = listOf(
@@ -39,6 +42,9 @@
     )
 
     override fun extraParams() = listOf(
-        getter(ParsedProcess::getDeniedPermissions, setOf("testDeniedPermission"))
+        getter(ParsedProcess::getDeniedPermissions, setOf("testDeniedPermission")),
+        getter(ParsedProcess::getAppClassNamesByPackage, ArrayMap<String, String>().apply {
+            put("package1", "classname1");
+        }),
     )
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
index d728451..189396f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeAppOpsHelper.java
@@ -31,7 +31,7 @@
     private static class AppOp {
         AppOp() {}
         boolean mAllowed = true;
-        boolean mStarted = false;
+        int mStarted = 0;
         int mNoteCount = 0;
     }
 
@@ -49,7 +49,7 @@
 
     public boolean isAppOpStarted(int appOp, String packageName) {
         AppOp myAppOp = getOp(packageName, appOp);
-        return myAppOp.mStarted;
+        return myAppOp.mStarted > 0;
     }
 
     public int getAppOpNoteCount(int appOp, String packageName) {
@@ -63,16 +63,15 @@
         if (!myAppOp.mAllowed) {
             return false;
         }
-        Preconditions.checkState(!myAppOp.mStarted);
-        myAppOp.mStarted = true;
+        myAppOp.mStarted++;
         return true;
     }
 
     @Override
     public void finishOp(int appOp, CallerIdentity callerIdentity) {
         AppOp myAppOp = getOp(callerIdentity.getPackageName(), appOp);
-        Preconditions.checkState(myAppOp.mStarted);
-        myAppOp.mStarted = false;
+        Preconditions.checkState(myAppOp.mStarted > 0);
+        myAppOp.mStarted--;
     }
 
     @Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java
deleted file mode 100644
index 94dcdf9..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/LocationAttributionHelperTest.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2020 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.location.injector;
-
-import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
-import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.location.util.identity.CallerIdentity;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LocationAttributionHelperTest {
-
-    @Mock private AppOpsHelper mAppOpsHelper;
-
-    private LocationAttributionHelper mHelper;
-
-    @Before
-    public void setUp() {
-        initMocks(this);
-
-        when(mAppOpsHelper.startOpNoThrow(anyInt(), any(CallerIdentity.class))).thenReturn(true);
-
-        mHelper = new LocationAttributionHelper(mAppOpsHelper);
-    }
-
-    @Test
-    public void testLocationMonitoring() {
-        CallerIdentity caller1 = CallerIdentity.forTest(1, 1, "test1", null);
-        CallerIdentity caller2 = CallerIdentity.forTest(2, 2, "test2", null);
-
-        mHelper.reportLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportLocationStop(caller1);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        mHelper.reportLocationStop(caller1);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_LOCATION, CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportLocationStop(caller2);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        mHelper.reportLocationStop(caller2);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_LOCATION, CallerIdentity.forAggregation(caller2));
-    }
-
-    @Test
-    public void testHighPowerLocationMonitoring() {
-        CallerIdentity caller1 = CallerIdentity.forTest(1, 1, "test1", null);
-        CallerIdentity caller2 = CallerIdentity.forTest(2, 2, "test2", null);
-
-        mHelper.reportHighPowerLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStart(caller1);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportHighPowerLocationStart(caller2);
-        verify(mAppOpsHelper).startOpNoThrow(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-
-        mHelper.reportHighPowerLocationStop(caller1);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-        mHelper.reportHighPowerLocationStop(caller1);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller1));
-
-        mHelper.reportHighPowerLocationStop(caller2);
-        verify(mAppOpsHelper, never()).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-        mHelper.reportHighPowerLocationStop(caller2);
-        verify(mAppOpsHelper).finishOp(OP_MONITOR_HIGH_POWER_LOCATION,
-                CallerIdentity.forAggregation(caller2));
-    }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
index bd24cfd..02cacb7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java
@@ -33,7 +33,6 @@
     private final FakeScreenInteractiveHelper mScreenInteractiveHelper;
     private final FakeDeviceStationaryHelper mDeviceStationaryHelper;
     private final FakeDeviceIdleHelper mDeviceIdleHelper;
-    private final LocationAttributionHelper mLocationAttributionHelper;
     private final FakeEmergencyHelper mEmergencyHelper;
     private final LocationUsageLogger mLocationUsageLogger;
 
@@ -49,7 +48,6 @@
         mScreenInteractiveHelper = new FakeScreenInteractiveHelper();
         mDeviceStationaryHelper = new FakeDeviceStationaryHelper();
         mDeviceIdleHelper = new FakeDeviceIdleHelper();
-        mLocationAttributionHelper = new LocationAttributionHelper(mAppOpsHelper);
         mEmergencyHelper = new FakeEmergencyHelper();
         mLocationUsageLogger = new LocationUsageLogger();
     }
@@ -110,11 +108,6 @@
     }
 
     @Override
-    public LocationAttributionHelper getLocationAttributionHelper() {
-        return mLocationAttributionHelper;
-    }
-
-    @Override
     public EmergencyHelper getEmergencyHelper() {
         return mEmergencyHelper;
     }
diff --git a/tests/MultiUser/Android.bp b/tests/MultiUser/Android.bp
new file mode 100644
index 0000000..bde309f
--- /dev/null
+++ b/tests/MultiUser/Android.bp
@@ -0,0 +1,27 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MultiUserTests",
+    platform_apis: true,
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.runner",
+        "services.core",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    certificate: "platform",
+    test_suites: ["device-tests"],
+}
diff --git a/tests/MultiUser/AndroidManifest.xml b/tests/MultiUser/AndroidManifest.xml
new file mode 100644
index 0000000..9a25c09
--- /dev/null
+++ b/tests/MultiUser/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.test.multiuser">
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.MANAGE_USERS" />
+
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.test.multiuser">
+    </instrumentation>
+</manifest>
diff --git a/tests/MultiUser/TEST_MAPPING b/tests/MultiUser/TEST_MAPPING
new file mode 100644
index 0000000..0dbef6c
--- /dev/null
+++ b/tests/MultiUser/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "MultiUserTests"
+    }
+  ]
+}
diff --git a/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.java b/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.java
new file mode 100644
index 0000000..1521cc6
--- /dev/null
+++ b/tests/MultiUser/src/com/android/test/multiuser/MultiUserSettingsTests.java
@@ -0,0 +1,164 @@
+/*
+ * 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.test.multiuser;
+
+import static android.provider.Settings.Secure.FONT_WEIGHT_ADJUSTMENT;
+import static android.provider.Settings.System.FONT_SCALE;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiUserSettingsTests {
+    private final Context mContext = getInstrumentation().getTargetContext();
+    private final ContentResolver mContentResolver = mContext.getContentResolver();
+
+    private static void waitForBroadcastIdle() throws InterruptedException {
+        final int sleepDuration = 1000;
+        final String cmdAmWaitForBroadcastIdle = "am wait-for-broadcast-idle";
+
+        Thread.sleep(sleepDuration);
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .executeShellCommand(cmdAmWaitForBroadcastIdle);
+        Thread.sleep(sleepDuration);
+    }
+
+    private float getGlobalFontScale() {
+        return mContext.getResources().getConfiguration().fontScale;
+    }
+
+    private int getGlobalFontWeight() {
+        return mContext.getResources().getConfiguration().fontWeightAdjustment;
+    }
+
+    private float getFontScaleOfUser(int userId) {
+        return Settings.System.getFloatForUser(mContentResolver, FONT_SCALE, 1, userId);
+    }
+
+    private int getFontWeightOfUser(int userId) {
+        return Settings.Secure.getIntForUser(mContentResolver, FONT_WEIGHT_ADJUSTMENT, 1, userId);
+    }
+
+    private void setFontScaleOfUser(float fontScale, int userId) throws InterruptedException {
+        Settings.System.putFloatForUser(mContentResolver, FONT_SCALE, fontScale, userId);
+        waitForBroadcastIdle();
+    }
+
+    private void setFontWeightOfUser(int fontWeight, int userId) throws InterruptedException {
+        Settings.Secure.putIntForUser(mContentResolver, FONT_WEIGHT_ADJUSTMENT, fontWeight, userId);
+        waitForBroadcastIdle();
+    }
+
+    @Test
+    public void testChangingFontScaleOfABackgroundUser_shouldNotAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        UserManager userManager = UserManager.get(mContext);
+
+        final int backgroundUserId = userManager.createUser("test_user",
+                UserManager.USER_TYPE_FULL_SECONDARY, 0).id;
+        final float oldFontScaleOfBgUser = getFontScaleOfUser(backgroundUserId);
+        final float oldGlobalFontScale = getGlobalFontScale();
+        final float newFontScaleOfBgUser = 1 + Math.max(oldGlobalFontScale, oldFontScaleOfBgUser);
+
+        try {
+            setFontScaleOfUser(newFontScaleOfBgUser, backgroundUserId);
+            final float newGlobalFontScale = getGlobalFontScale();
+            assertEquals(oldGlobalFontScale, newGlobalFontScale, 0);
+        } finally {
+            setFontScaleOfUser(oldFontScaleOfBgUser, backgroundUserId);
+            userManager.removeUser(backgroundUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontWeightOfABackgroundUser_shouldNotAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        UserManager userManager = UserManager.get(mContext);
+
+        final int backgroundUserId = userManager.createUser("test_user",
+                UserManager.USER_TYPE_FULL_SECONDARY, 0).id;
+        final int oldFontWeightOfBgUser = getFontWeightOfUser(backgroundUserId);
+        final int oldGlobalFontWeight = getGlobalFontWeight();
+        final int newFontWeightOfBgUser = 2 * Math.max(oldGlobalFontWeight, oldFontWeightOfBgUser);
+
+        try {
+            setFontWeightOfUser(newFontWeightOfBgUser, backgroundUserId);
+            final int newGlobalFontWeight = getGlobalFontWeight();
+            assertEquals(oldGlobalFontWeight, newGlobalFontWeight);
+        } finally {
+            setFontWeightOfUser(oldFontWeightOfBgUser, backgroundUserId);
+            userManager.removeUser(backgroundUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontScaleOfTheForegroundUser_shouldAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        final int currentUserId = mContext.getUserId();
+        final float oldFontScale = getFontScaleOfUser(currentUserId);
+        final float newFontScale = 1 + oldFontScale;
+
+        try {
+            setFontScaleOfUser(newFontScale, currentUserId);
+            final float globalFontScale = getGlobalFontScale();
+            assertEquals(newFontScale, globalFontScale, 0);
+        } finally {
+            setFontScaleOfUser(oldFontScale, currentUserId);
+        }
+    }
+
+    @Test
+    public void testChangingFontWeightOfTheForegroundUser_shouldAffectUI()
+            throws InterruptedException {
+
+        Assume.assumeTrue(UserManager.supportsMultipleUsers());
+
+        final int currentUserId = mContext.getUserId();
+        final int oldFontWeight = getFontWeightOfUser(currentUserId);
+        final int newFontWeight = 2 * oldFontWeight;
+
+        try {
+            setFontWeightOfUser(newFontWeight, currentUserId);
+            final int globalFontWeight = getGlobalFontWeight();
+            assertEquals(newFontWeight, globalFontWeight);
+        } finally {
+            setFontWeightOfUser(oldFontWeight, currentUserId);
+        }
+    }
+}
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index d204190..d432341 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -588,10 +588,21 @@
             child_el->name == "provider" || child_el->name == "receiver" ||
             child_el->name == "service") {
           FullyQualifyClassName(original_package, xml::kSchemaAndroid, "name", child_el);
+          continue;
         }
 
         if (child_el->name == "activity-alias") {
           FullyQualifyClassName(original_package, xml::kSchemaAndroid, "targetActivity", child_el);
+          continue;
+        }
+
+        if (child_el->name == "processes") {
+          for (xml::Element* grand_child_el : child_el->GetChildElements()) {
+            if (grand_child_el->name == "process") {
+              FullyQualifyClassName(original_package, xml::kSchemaAndroid, "name", grand_child_el);
+            }
+          }
+          continue;
         }
       }
     }