Merge "Fix an IllegalStateException in the OverlayConfigParser"
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 40ab49c..9715b85 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -83,6 +83,8 @@
     static final long UNUSED_RECLAMATION_PERIOD_MS = 24 * HOUR_IN_MILLIS;
     /** How much of an app's unused wealth should be reclaimed periodically. */
     private static final float DEFAULT_UNUSED_RECLAMATION_PERCENTAGE = .1f;
+    /** The amount of time to delay reclamation by after boot. */
+    private static final long RECLAMATION_STARTUP_DELAY_MS = 30_000L;
     private static final int PACKAGE_QUERY_FLAGS =
             PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                     | PackageManager.MATCH_APEX;
@@ -99,6 +101,7 @@
     private final CompleteEconomicPolicy mCompleteEconomicPolicy;
     private final ConfigObserver mConfigObserver;
     private final EconomyManagerStub mEconomyManagerStub;
+    private final Scribe mScribe;
 
     @NonNull
     @GuardedBy("mLock")
@@ -117,9 +120,6 @@
     // In the range [0,100] to represent 0% to 100% battery.
     @GuardedBy("mLock")
     private int mCurrentBatteryLevel;
-    // TODO: load from disk
-    @GuardedBy("mLock")
-    private long mLastUnusedReclamationTime;
 
     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Nullable
@@ -187,7 +187,7 @@
                 public void onAlarm() {
                     synchronized (mLock) {
                         mAgent.reclaimUnusedAssetsLocked(DEFAULT_UNUSED_RECLAMATION_PERCENTAGE);
-                        mLastUnusedReclamationTime = getCurrentTimeMillis();
+                        mScribe.setLastReclamationTimeLocked(getCurrentTimeMillis());
                         scheduleUnusedWealthReclamationLocked();
                     }
                 }
@@ -216,6 +216,7 @@
         mPackageManager = context.getPackageManager();
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mEconomyManagerStub = new EconomyManagerStub();
+        mScribe = new Scribe(this);
         mCompleteEconomicPolicy = new CompleteEconomicPolicy(this);
         mAgent = new Agent(this, mCompleteEconomicPolicy);
 
@@ -451,8 +452,8 @@
     @GuardedBy("mLock")
     private void scheduleUnusedWealthReclamationLocked() {
         final long now = getCurrentTimeMillis();
-        final long nextReclamationTime =
-                Math.max(mLastUnusedReclamationTime + UNUSED_RECLAMATION_PERIOD_MS, now + 30_000);
+        final long nextReclamationTime = Math.max(now + RECLAMATION_STARTUP_DELAY_MS,
+                mScribe.getLastReclamationTimeLocked() + UNUSED_RECLAMATION_PERIOD_MS);
         mHandler.post(() -> {
             // Never call out to AlarmManager with the lock held. This sits below AM.
             AlarmManager alarmManager = getContext().getSystemService(AlarmManager.class);
@@ -463,7 +464,7 @@
                         ALARM_TAG_WEALTH_RECLAMATION, mUnusedWealthReclamationListener, mHandler);
             } else {
                 mHandler.sendEmptyMessageDelayed(
-                        MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT, 30_000);
+                        MSG_SCHEDULE_UNUSED_WEALTH_RECLAMATION_EVENT, RECLAMATION_STARTUP_DELAY_MS);
             }
         });
     }
@@ -531,6 +532,7 @@
             if (isFirstSetup) {
                 mAgent.grantBirthrightsLocked();
             }
+            scheduleUnusedWealthReclamationLocked();
         }
     }
 
@@ -542,7 +544,6 @@
             registerListeners();
             mCurrentBatteryLevel = getCurrentBatteryLevel();
             mHandler.post(this::setupHeavyWork);
-            scheduleUnusedWealthReclamationLocked();
             mCompleteEconomicPolicy.setup();
         }
     }
@@ -562,6 +563,7 @@
                 }
             });
             mPkgCache.clear();
+            mScribe.tearDownLocked();
             mUidToPackageCache.clear();
             getContext().unregisterReceiver(mBroadcastReceiver);
             UsageStatsManagerInternal usmi =
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
new file mode 100644
index 0000000..a2c1e9a
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tare;
+
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Maintains the current TARE state and handles writing it to disk and reading it back from disk.
+ */
+public class Scribe {
+    private static final String TAG = "TARE-" + Scribe.class.getSimpleName();
+    private static final boolean DEBUG = InternalResourceService.DEBUG
+            || Log.isLoggable(TAG, Log.DEBUG);
+
+    private final InternalResourceService mIrs;
+
+    @GuardedBy("mIrs.mLock")
+    private long mLastReclamationTime;
+
+    Scribe(InternalResourceService irs) {
+        mIrs = irs;
+    }
+
+    @GuardedBy("mIrs.mLock")
+    long getLastReclamationTimeLocked() {
+        return mLastReclamationTime;
+    }
+
+    @GuardedBy("InternalResourceService.mLock")
+    void setLastReclamationTimeLocked(long time) {
+        mLastReclamationTime = time;
+    }
+
+    @GuardedBy("mIrs.mLock")
+    void tearDownLocked() {
+        mLastReclamationTime = 0;
+    }
+}
diff --git a/core/api/current.txt b/core/api/current.txt
index 5178f3d83..b9ef02e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -79,6 +79,7 @@
     field public static final String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES";
     field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES";
     field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES";
+    field public static final String DELIVER_COMPANION_MESSAGES = "android.permission.DELIVER_COMPANION_MESSAGES";
     field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC";
     field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD";
     field public static final String DUMP = "android.permission.DUMP";
@@ -842,7 +843,7 @@
     field @Deprecated public static final int isModifier = 16843334; // 0x1010246
     field @Deprecated public static final int isRepeatable = 16843336; // 0x1010248
     field public static final int isScrollContainer = 16843342; // 0x101024e
-    field public static final int isSplitRequired = 16844177; // 0x1010591
+    field @Deprecated public static final int isSplitRequired = 16844177; // 0x1010591
     field public static final int isStatic = 16844122; // 0x101055a
     field @Deprecated public static final int isSticky = 16843335; // 0x1010247
     field public static final int isolatedProcess = 16843689; // 0x10103a9
@@ -9897,6 +9898,8 @@
     method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
     method @MainThread public abstract void onDeviceAppeared(@NonNull String);
     method @MainThread public abstract void onDeviceDisappeared(@NonNull String);
+    method @MainThread public void onSendMessage(int, int, @NonNull byte[]);
+    method @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void receiveMessage(int, int, @NonNull byte[]);
     field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
   }
 
@@ -46984,8 +46987,15 @@
   }
 
   @UiThread public interface AttachedSurfaceControl {
+    method public default void addOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
     method public boolean applyTransactionOnDraw(@NonNull android.view.SurfaceControl.Transaction);
     method @Nullable public android.view.SurfaceControl.Transaction buildReparentTransaction(@NonNull android.view.SurfaceControl);
+    method public default int getSurfaceTransformHint();
+    method public default void removeOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
+  }
+
+  @UiThread public static interface AttachedSurfaceControl.OnSurfaceTransformHintChangedListener {
+    method public void onSurfaceTransformHintChanged(int);
   }
 
   public final class Choreographer {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e110341..57cc756 100755
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14399,6 +14399,7 @@
     method public int getFlags();
     method @Nullable public android.view.contentcapture.ContentCaptureSessionId getParentSessionId();
     method public int getTaskId();
+    method @Nullable public android.os.IBinder getWindowToken();
     field public static final int FLAG_DISABLED_BY_APP = 1; // 0x1
     field public static final int FLAG_DISABLED_BY_FLAG_SECURE = 2; // 0x2
     field public static final int FLAG_RECONNECTED = 4; // 0x4
@@ -14406,6 +14407,7 @@
 
   public final class ContentCaptureEvent implements android.os.Parcelable {
     method public int describeContents();
+    method @Nullable public android.graphics.Rect getBounds();
     method @Nullable public android.view.contentcapture.ContentCaptureContext getContentCaptureContext();
     method public long getEventTime();
     method @Nullable public android.view.autofill.AutofillId getId();
@@ -14425,6 +14427,7 @@
     field public static final int TYPE_VIEW_TEXT_CHANGED = 3; // 0x3
     field public static final int TYPE_VIEW_TREE_APPEARED = 5; // 0x5
     field public static final int TYPE_VIEW_TREE_APPEARING = 4; // 0x4
+    field public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10; // 0xa
   }
 
   public final class ContentCaptureManager {
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 6a527a5..a9e9ba5 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -445,6 +445,35 @@
     }
 
     /**
+     * Dispatch received message to system for processing.
+     *
+     * <p>Messages received from {@link CompanionDeviceService#onSendMessage}, which is implemented
+     * on another device, need to be dispatched to system for processing.</p>
+     *
+     * <p>Calling app must declare uses-permission
+     * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}</p>
+     *
+     * @param messageId id of the message
+     * @param associationId association id of the associated device where data is coming from
+     * @param message message received from the associated device
+     *
+     * @throws DeviceNotAssociatedException if the given device was not previously associated with
+     * this app
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
+    public void receiveMessage(int messageId, int associationId, @NonNull byte[] message)
+            throws DeviceNotAssociatedException {
+        try {
+            mService.receiveMessage(messageId, associationId, message);
+        } catch (RemoteException e) {
+            ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class);
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Associates given device with given app for the given user directly, without UI prompt.
      *
      * @param packageName package name of the companion app
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 56639ea..86d6b23 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -20,6 +20,7 @@
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.app.Service;
 import android.content.Intent;
 import android.os.Handler;
@@ -86,6 +87,36 @@
     @MainThread
     public abstract void onDeviceDisappeared(@NonNull String address);
 
+    /**
+     * Called by system whenever the system tries to send a message to an associated device.
+     *
+     * @param messageId system assigned id of the message to be sent
+     * @param associationId association id of the associated device
+     * @param message message to be sent
+     */
+    @MainThread
+    public void onSendMessage(int messageId, int associationId, @NonNull byte[] message) {
+        // do nothing. Companion apps can override this function for system to send messages.
+    }
+
+    /**
+     * Called when there's message received from an associated device, which needs to be dispatched
+     * to system for processing.
+     *
+     * <p>Calling app must declare uses-permission
+     * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}</p>
+     *
+     * @param messageId id of the message
+     * @param associationId id of the associated device
+     * @param message messaged received from the associated device
+     */
+    @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
+    public final void receiveMessage(int messageId, int associationId, @NonNull byte[] message) {
+        CompanionDeviceManager companionDeviceManager =
+                getSystemService(CompanionDeviceManager.class);
+        companionDeviceManager.receiveMessage(messageId, associationId, message);
+    }
+
     @Nullable
     @Override
     public final IBinder onBind(@NonNull Intent intent) {
@@ -112,5 +143,18 @@
                     CompanionDeviceService::onDeviceDisappeared,
                     CompanionDeviceService.this, address));
         }
+
+        public void onSendMessage(int messageId, int associationId, @NonNull byte[] message) {
+            Handler.getMain().sendMessage(PooledLambda.obtainMessage(
+                    CompanionDeviceService::onSendMessage,
+                    CompanionDeviceService.this, messageId, associationId, message));
+        }
+
+        public final void receiveMessage(int messageId, int associationId,
+                @NonNull byte[] message) {
+            Handler.getMain().sendMessage(PooledLambda.obtainMessage(
+                    CompanionDeviceService::receiveMessage,
+                    CompanionDeviceService.this, messageId, associationId, message));
+        }
     }
 }
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index d113b92..9c2bcf2 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -54,4 +54,6 @@
 
     void createAssociation(in String packageName, in String macAddress, int userId,
         in byte[] certificate);
+
+    void receiveMessage(in int messageId, in int associationId, in byte[] message);
 }
diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl
index f3977ca..921c124 100644
--- a/core/java/android/companion/ICompanionDeviceService.aidl
+++ b/core/java/android/companion/ICompanionDeviceService.aidl
@@ -16,14 +16,9 @@
 
 package android.companion;
 
-import android.app.PendingIntent;
-import android.companion.IFindDeviceCallback;
-import android.companion.Association;
-import android.companion.AssociationRequest;
-import android.content.ComponentName;
-
 /** @hide */
 oneway interface ICompanionDeviceService {
     void onDeviceAppeared(in String address);
     void onDeviceDisappeared(in String address);
+    void onSendMessage(in int messageId, in int associationId, in byte[] message);
 }
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index eab9d8b..0276815 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -85,7 +85,7 @@
 
     /**
      * A {@code Key} is used to do capture request field lookups with
-     * {@link CaptureResult#get} or to set fields with
+     * {@link CaptureRequest#get} or to set fields with
      * {@link CaptureRequest.Builder#set(Key, Object)}.
      *
      * <p>For example, to set the crop rectangle for the next capture:
@@ -95,11 +95,11 @@
      * </pre></code>
      * </p>
      *
-     * <p>To enumerate over all possible keys for {@link CaptureResult}, see
-     * {@link CameraCharacteristics#getAvailableCaptureResultKeys}.</p>
+     * <p>To enumerate over all possible keys for {@link CaptureRequest}, see
+     * {@link CameraCharacteristics#getAvailableCaptureRequestKeys}.</p>
      *
-     * @see CaptureResult#get
-     * @see CameraCharacteristics#getAvailableCaptureResultKeys
+     * @see CaptureRequest#get
+     * @see CameraCharacteristics#getAvailableCaptureRequestKeys
      */
     public final static class Key<T> {
         private final CameraMetadataNative.Key<T> mKey;
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index a25ae60..d9734b4 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -1018,7 +1018,7 @@
      * <p>The following formats will never have a stall duration:
      * <ul>
      * <li>{@link ImageFormat#YUV_420_888 YUV_420_888}
-     * <li>{@link #isOutputSupportedFor(Class) Implementation-Defined}
+     * <li>{@link ImageFormat#PRIVATE PRIVATE}
      * </ul></p>
      *
      * <p>
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index d48d562..a3d595c 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -146,6 +146,7 @@
     private CryptoObject mCryptoObject;
     @Nullable private RemoveTracker mRemoveTracker;
     private Handler mHandler;
+    @Nullable private float[] mEnrollStageThresholds;
 
     /**
      * Retrieves a list of properties for all fingerprint sensors on the device.
@@ -1329,6 +1330,46 @@
     /**
      * @hide
      */
+    public int getEnrollStageCount() {
+        if (mEnrollStageThresholds == null) {
+            mEnrollStageThresholds = createEnrollStageThresholds(mContext);
+        }
+        return mEnrollStageThresholds.length + 1;
+    }
+
+    /**
+     * @hide
+     */
+    public float getEnrollStageThreshold(int index) {
+        if (mEnrollStageThresholds == null) {
+            mEnrollStageThresholds = createEnrollStageThresholds(mContext);
+        }
+
+        if (index < 0 || index > mEnrollStageThresholds.length) {
+            Slog.w(TAG, "Unsupported enroll stage index: " + index);
+            return index < 0 ? 0f : 1f;
+        }
+
+        // The implicit threshold for the final stage is always 1.
+        return index == mEnrollStageThresholds.length ? 1f : mEnrollStageThresholds[index];
+    }
+
+    @NonNull
+    private static float[] createEnrollStageThresholds(@NonNull Context context) {
+        // TODO(b/200604947): Fetch this value from FingerprintService, rather than internal config
+        final String[] enrollStageThresholdStrings = context.getResources().getStringArray(
+                com.android.internal.R.array.config_udfps_enroll_stage_thresholds);
+
+        final float[] enrollStageThresholds = new float[enrollStageThresholdStrings.length];
+        for (int i = 0; i < enrollStageThresholds.length; i++) {
+            enrollStageThresholds[i] = Float.parseFloat(enrollStageThresholdStrings[i]);
+        }
+        return enrollStageThresholds;
+    }
+
+    /**
+     * @hide
+     */
     public static String getErrorString(Context context, int errMsg, int vendorCode) {
         switch (errMsg) {
             case FINGERPRINT_ERROR_HW_UNAVAILABLE:
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 491a5f9..a47c3b1 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -920,13 +920,6 @@
 
         if (!mWaking && !mFinished) {
             mWaking = true;
-            // During wake up the activity should be translucent to allow the application underneath
-            // to start drawing. Normally, the WM animation system takes care of this, but here we
-            // give the dream application some time to perform a custom exit animation.
-            // If it uses a view animation, the WM doesn't know about it and can't make the activity
-            // translucent in the normal way. Therefore, here we ensure that the activity is
-            // translucent during wake up regardless of what animation is used in onWakeUp().
-            mActivity.convertToTranslucent(null, null);
 
             // As a minor optimization, invoke the callback first in case it simply
             // calls finish() immediately so there wouldn't be much point in telling
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index bcc5b56..b2fc9a0 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -53,4 +53,72 @@
      * to the View hierarchy you may need to call {@link android.view.View#invalidate}
      */
     boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t);
+
+    /**
+     * The transform hint can be used by a buffer producer to pre-rotate the rendering such that the
+     * final transformation in the system composer is identity. This can be very useful when used in
+     * conjunction with the h/w composer HAL in situations where it cannot handle rotations or
+     * handle them with an additional power cost.
+     *
+     * The transform hint should be used with ASurfaceControl APIs when submitting buffers.
+     * Example usage:
+     *
+     * 1. After a configuration change, before dequeuing a buffer, the buffer producer queries the
+     *    function for the transform hint.
+     *
+     * 2. The desired buffer width and height is rotated by the transform hint.
+     *
+     * 3. The producer dequeues a buffer of the new pre-rotated size.
+     *
+     * 4. The producer renders to the buffer such that the image is already transformed, that is
+     *    applying the transform hint to the rendering.
+     *
+     * 5. The producer applies the inverse transform hint to the buffer it just rendered.
+     *
+     * 6. The producer queues the pre-transformed buffer with the buffer transform.
+     *
+     * 7. The composer combines the buffer transform with the display transform.  If the buffer
+     *    transform happens to cancel out the display transform then no rotation is needed and there
+     *    will be no performance penalties.
+     *
+     * Note, when using ANativeWindow APIs in conjunction with a NativeActivity Surface or
+     * SurfaceView Surface, the buffer producer will already have access to the transform hint and
+     * no additional work is needed.
+     */
+    default @Surface.Rotation int getSurfaceTransformHint() {
+        return Surface.ROTATION_0;
+    }
+
+    /**
+     * Surface transform hint change listener.
+     * @see #getSurfaceTransformHint
+     */
+    @UiThread
+    interface OnSurfaceTransformHintChangedListener {
+        /**
+         * @param hint new surface transform hint
+         * @see #getSurfaceTransformHint
+         */
+        void onSurfaceTransformHintChanged(@Surface.Rotation int hint);
+    }
+
+    /**
+     * Registers a surface transform hint changed listener to receive notifications about when
+     * the transform hint changes.
+     *
+     * @see #getSurfaceTransformHint
+     * @see #removeOnSurfaceTransformHintChangedListener
+     */
+    default void addOnSurfaceTransformHintChangedListener(
+            @NonNull OnSurfaceTransformHintChangedListener listener) {
+    }
+
+    /**
+     * Unregisters a surface transform hint changed listener.
+     *
+     * @see #addOnSurfaceTransformHintChangedListener
+     */
+    default void removeOnSurfaceTransformHintChangedListener(
+            @NonNull OnSurfaceTransformHintChangedListener listener) {
+    }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 0dd6e22..5c97f81 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -312,6 +312,9 @@
     static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
     static boolean sFirstDrawComplete = false;
 
+    private ArrayList<OnSurfaceTransformHintChangedListener> mTransformHintListeners =
+            new ArrayList<>();
+    private @Surface.Rotation int mPreviousTransformHint = Surface.ROTATION_0;
     /**
      * Callback for notifying about global configuration changes.
      */
@@ -4269,6 +4272,14 @@
         try {
             if (!isContentCaptureEnabled()) return;
 
+            // Initial dispatch of window bounds to content capture
+            if (mAttachInfo.mContentCaptureManager != null) {
+                MainContentCaptureSession session =
+                        mAttachInfo.mContentCaptureManager.getMainContentCaptureSession();
+                session.notifyWindowBoundsChanged(session.getId(),
+                        getConfiguration().windowConfiguration.getBounds());
+            }
+
             // Content capture is a go!
             rootView.dispatchInitialProvideContentCaptureStructure();
         } finally {
@@ -7803,6 +7814,14 @@
                 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                 mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
                 mTempControls, mSurfaceSize);
+
+        if (mAttachInfo.mContentCaptureManager != null) {
+            MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
+                    .getMainContentCaptureSession();
+            mainSession.notifyWindowBoundsChanged(mainSession.getId(),
+                    getConfiguration().windowConfiguration.getBounds());
+        }
+
         mPendingBackDropFrame.set(mTmpFrames.backdropFrame);
         if (mSurfaceControl.isValid()) {
             if (!useBLAST()) {
@@ -7820,6 +7839,11 @@
                 mAttachInfo.mThreadedRenderer.setSurfaceControl(mSurfaceControl);
                 mAttachInfo.mThreadedRenderer.setBlastBufferQueue(mBlastBufferQueue);
             }
+            int transformHint = mSurfaceControl.getTransformHint();
+            if (mPreviousTransformHint != transformHint) {
+                mPreviousTransformHint = transformHint;
+                dispatchTransformHintChanged(transformHint);
+            }
         } else {
             destroySurface();
         }
@@ -10442,7 +10466,39 @@
         return true;
     }
 
-    int getSurfaceTransformHint() {
+    @Override
+    public @Surface.Rotation int getSurfaceTransformHint() {
         return mSurfaceControl.getTransformHint();
     }
+
+    @Override
+    public void addOnSurfaceTransformHintChangedListener(
+            OnSurfaceTransformHintChangedListener listener) {
+        Objects.requireNonNull(listener);
+        if (mTransformHintListeners.contains(listener)) {
+            throw new IllegalArgumentException(
+                    "attempt to call addOnSurfaceTransformHintChangedListener() "
+                            + "with a previously registered listener");
+        }
+        mTransformHintListeners.add(listener);
+    }
+
+    @Override
+    public void removeOnSurfaceTransformHintChangedListener(
+            OnSurfaceTransformHintChangedListener listener) {
+        Objects.requireNonNull(listener);
+        mTransformHintListeners.remove(listener);
+    }
+
+    private void dispatchTransformHintChanged(@Surface.Rotation int hint) {
+        if (mTransformHintListeners.isEmpty()) {
+            return;
+        }
+        ArrayList<OnSurfaceTransformHintChangedListener> listeners =
+                (ArrayList<OnSurfaceTransformHintChangedListener>) mTransformHintListeners.clone();
+        for (int i = 0; i < listeners.size(); i++) {
+            OnSurfaceTransformHintChangedListener listener = listeners.get(i);
+            listener.onSurfaceTransformHintChanged(hint);
+        }
+    }
 }
diff --git a/core/java/android/view/contentcapture/ContentCaptureContext.java b/core/java/android/view/contentcapture/ContentCaptureContext.java
index 71b8003..3bc9a96 100644
--- a/core/java/android/view/contentcapture/ContentCaptureContext.java
+++ b/core/java/android/view/contentcapture/ContentCaptureContext.java
@@ -27,6 +27,7 @@
 import android.content.Context;
 import android.content.LocusId;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Display;
@@ -105,6 +106,7 @@
     private final int mFlags;
     private final int mDisplayId;
     private final ActivityId mActivityId;
+    private final IBinder mWindowToken;
 
     // Fields below are set by the service upon "delivery" and are not marshalled in the parcel
     private int mParentSessionId = NO_SESSION_ID;
@@ -112,7 +114,7 @@
     /** @hide */
     public ContentCaptureContext(@Nullable ContentCaptureContext clientContext,
             @NonNull ActivityId activityId, @NonNull ComponentName componentName, int displayId,
-            int flags) {
+            IBinder windowToken, int flags) {
         if (clientContext != null) {
             mHasClientContext = true;
             mExtras = clientContext.mExtras;
@@ -126,6 +128,7 @@
         mFlags = flags;
         mDisplayId = displayId;
         mActivityId = activityId;
+        mWindowToken = windowToken;
     }
 
     private ContentCaptureContext(@NonNull Builder builder) {
@@ -137,6 +140,7 @@
         mFlags = 0;
         mDisplayId = Display.INVALID_DISPLAY;
         mActivityId = null;
+        mWindowToken = null;
     }
 
     /** @hide */
@@ -148,6 +152,7 @@
         mFlags = original.mFlags | extraFlags;
         mDisplayId = original.mDisplayId;
         mActivityId = original.mActivityId;
+        mWindowToken = original.mWindowToken;
     }
 
     /**
@@ -230,6 +235,20 @@
     }
 
     /**
+     * Gets the window token of the activity associated with this context.
+     *
+     * <p>The token can be used to attach relevant overlay views to the activity's window. This can
+     * be done through {@link android.view.WindowManager.LayoutParams#token}.
+     *
+     * @hide
+     */
+    @SystemApi
+    @Nullable
+    public IBinder getWindowToken() {
+        return mWindowToken;
+    }
+
+    /**
      * Gets the flags associated with this context.
      *
      * @return any combination of {@link #FLAG_DISABLED_BY_FLAG_SECURE},
@@ -328,6 +347,7 @@
         }
         pw.print(", activityId="); pw.print(mActivityId);
         pw.print(", displayId="); pw.print(mDisplayId);
+        pw.print(", windowToken="); pw.print(mWindowToken);
         if (mParentSessionId != NO_SESSION_ID) {
             pw.print(", parentId="); pw.print(mParentSessionId);
         }
@@ -352,6 +372,7 @@
             builder.append("act=").append(ComponentName.flattenToShortString(mComponentName))
                 .append(", activityId=").append(mActivityId)
                 .append(", displayId=").append(mDisplayId)
+                .append(", windowToken=").append(mWindowToken)
                 .append(", flags=").append(mFlags);
         } else {
             builder.append("id=").append(mId);
@@ -381,6 +402,7 @@
         parcel.writeParcelable(mComponentName, flags);
         if (fromServer()) {
             parcel.writeInt(mDisplayId);
+            parcel.writeStrongBinder(mWindowToken);
             parcel.writeInt(mFlags);
             mActivityId.writeToParcel(parcel, flags);
         }
@@ -411,11 +433,12 @@
                 return clientContext;
             } else {
                 final int displayId = parcel.readInt();
+                final IBinder windowToken = parcel.readStrongBinder();
                 final int flags = parcel.readInt();
                 final ActivityId activityId = new ActivityId(parcel);
 
                 return new ContentCaptureContext(clientContext, activityId, componentName,
-                        displayId, flags);
+                        displayId, windowToken, flags);
             }
         }
 
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index ae45c6e..0f4bc19 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.Selection;
@@ -121,6 +122,12 @@
      */
     public static final int TYPE_VIEW_INSETS_CHANGED = 9;
 
+    /**
+     * Called before {@link #TYPE_VIEW_TREE_APPEARING}, or after the size of the window containing
+     * the views changed.
+     */
+    public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10;
+
     /** @hide */
     @IntDef(prefix = { "TYPE_" }, value = {
             TYPE_VIEW_APPEARED,
@@ -131,7 +138,8 @@
             TYPE_CONTEXT_UPDATED,
             TYPE_SESSION_PAUSED,
             TYPE_SESSION_RESUMED,
-            TYPE_VIEW_INSETS_CHANGED
+            TYPE_VIEW_INSETS_CHANGED,
+            TYPE_WINDOW_BOUNDS_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface EventType{}
@@ -149,6 +157,7 @@
     private int mParentSessionId = NO_SESSION_ID;
     private @Nullable ContentCaptureContext mClientContext;
     private @Nullable Insets mInsets;
+    private @Nullable Rect mBounds;
 
     private int mComposingStart = MAX_INVALID_VALUE;
     private int mComposingEnd = MAX_INVALID_VALUE;
@@ -345,6 +354,13 @@
         return this;
     }
 
+    /** @hide */
+    @NonNull
+    public ContentCaptureEvent setBounds(@NonNull Rect bounds) {
+        mBounds = bounds;
+        return this;
+    }
+
     /**
      * Gets the type of the event.
      *
@@ -418,6 +434,16 @@
     }
 
     /**
+     * Gets the {@link Rect} bounds of the window associated with the event. Valid bounds will only
+     * be returned if the type of the event is {@link #TYPE_WINDOW_BOUNDS_CHANGED}, otherwise they
+     * will be null.
+     */
+    @Nullable
+    public Rect getBounds() {
+        return mBounds;
+    }
+
+    /**
      * Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED}
      * or {@link #TYPE_VIEW_DISAPPEARED}.
      *
@@ -488,6 +514,9 @@
         if (mInsets != null) {
             pw.print(", insets="); pw.println(mInsets);
         }
+        if (mBounds != null) {
+            pw.print(", bounds="); pw.println(mBounds);
+        }
         if (mComposingStart > MAX_INVALID_VALUE) {
             pw.print(", composing("); pw.print(mComposingStart);
             pw.print(", "); pw.print(mComposingEnd); pw.print(")");
@@ -532,6 +561,9 @@
         if (mInsets != null) {
             string.append(", insets=").append(mInsets);
         }
+        if (mBounds != null) {
+            string.append(", bounds=").append(mBounds);
+        }
         if (mComposingStart > MAX_INVALID_VALUE) {
             string.append(", composing=[")
                     .append(mComposingStart).append(",").append(mComposingEnd).append("]");
@@ -567,6 +599,9 @@
         if (mType == TYPE_VIEW_INSETS_CHANGED) {
             parcel.writeParcelable(mInsets, flags);
         }
+        if (mType == TYPE_WINDOW_BOUNDS_CHANGED) {
+            parcel.writeParcelable(mBounds, flags);
+        }
         if (mType == TYPE_VIEW_TEXT_CHANGED) {
             parcel.writeInt(mComposingStart);
             parcel.writeInt(mComposingEnd);
@@ -607,6 +642,9 @@
             if (type == TYPE_VIEW_INSETS_CHANGED) {
                 event.setInsets(parcel.readParcelable(null));
             }
+            if (type == TYPE_WINDOW_BOUNDS_CHANGED) {
+                event.setBounds(parcel.readParcelable(null));
+            }
             if (type == TYPE_VIEW_TEXT_CHANGED) {
                 event.setComposingIndex(parcel.readInt(), parcel.readInt());
                 event.restoreComposingSpan();
@@ -648,6 +686,8 @@
                 return "CONTEXT_UPDATED";
             case TYPE_VIEW_INSETS_CHANGED:
                 return "VIEW_INSETS_CHANGED";
+            case TYPE_WINDOW_BOUNDS_CHANGED:
+                return "TYPE_WINDOW_BOUNDS_CHANGED";
             default:
                 return "UKNOWN_TYPE: " + type;
         }
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 4cf5532..98ef4e7 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -26,6 +26,7 @@
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
 import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString;
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
@@ -38,6 +39,7 @@
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
@@ -776,6 +778,14 @@
                 .setClientContext(context), FORCE_FLUSH));
     }
 
+    /** public because is also used by ViewRootImpl */
+    public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
+        mHandler.post(() -> sendEvent(
+                new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
+                .setBounds(bounds)
+        ));
+    }
+
     @Override
     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
         super.dump(prefix, pw);
diff --git a/core/java/com/android/internal/view/ListViewCaptureHelper.java b/core/java/com/android/internal/view/ListViewCaptureHelper.java
index c25b4b5..f4a5b71 100644
--- a/core/java/com/android/internal/view/ListViewCaptureHelper.java
+++ b/core/java/com/android/internal/view/ListViewCaptureHelper.java
@@ -39,6 +39,12 @@
     private int mOverScrollMode;
 
     @Override
+    public boolean onAcceptSession(@NonNull ListView view) {
+        return view.isVisibleToUser()
+                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
+    }
+
+    @Override
     public void onPrepareForStart(@NonNull ListView view, Rect scrollBounds) {
         mScrollDelta = 0;
 
@@ -114,7 +120,6 @@
         return result;
     }
 
-
     @Override
     public void onPrepareForEnd(@NonNull ListView listView) {
         // Restore original position and state
diff --git a/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java
index d14adf6..64622f0 100644
--- a/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java
+++ b/core/java/com/android/internal/view/RecyclerViewCaptureHelper.java
@@ -44,6 +44,12 @@
     private int mOverScrollMode;
 
     @Override
+    public boolean onAcceptSession(@NonNull ViewGroup view) {
+        return view.isVisibleToUser()
+                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
+    }
+
+    @Override
     public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
         mScrollDelta = 0;
 
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
index 356cd6b..baf725d 100644
--- a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
+++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
@@ -67,10 +67,7 @@
      * @param view the view being captured
      * @return true if the callback should respond to a request with scroll bounds
      */
-    default boolean onAcceptSession(@NonNull V view) {
-        return view.isVisibleToUser()
-                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
-    }
+    boolean onAcceptSession(@NonNull V view);
 
     /**
      * Given a scroll capture request for a view, adjust the provided rect to cover the scrollable
diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
index a360f63..db7881f 100644
--- a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
+++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
@@ -43,6 +43,11 @@
     private boolean mScrollBarEnabled;
     private int mOverScrollMode;
 
+    public boolean onAcceptSession(@NonNull ViewGroup view) {
+        return view.isVisibleToUser()
+                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
+    }
+
     public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
         mStartScrollY = view.getScrollY();
         mOverScrollMode = view.getOverScrollMode();
diff --git a/core/java/com/android/internal/view/WebViewCaptureHelper.java b/core/java/com/android/internal/view/WebViewCaptureHelper.java
index e6a311c..37ce782 100644
--- a/core/java/com/android/internal/view/WebViewCaptureHelper.java
+++ b/core/java/com/android/internal/view/WebViewCaptureHelper.java
@@ -28,7 +28,7 @@
 /**
  * ScrollCapture for WebView.
  */
-class WebViewCaptureHelper implements ScrollCaptureViewHelper<WebView> {
+public class WebViewCaptureHelper implements ScrollCaptureViewHelper<WebView> {
     private static final String TAG = "WebViewScrollCapture";
 
     private final Rect mRequestWebViewLocal = new Rect();
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 20beaf7..c2c8239 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2950,13 +2950,11 @@
                 android:protectionLevel="signature" />
 
     <!-- Allows an app to set and release automotive projection.
-         <p>Once permissions can be granted via role-only, this needs to be changed to
-          protectionLevel="role" and added to the SYSTEM_AUTOMOTIVE_PROJECTION role.
          @hide
          @SystemApi
     -->
     <permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"
-                android:protectionLevel="signature|privileged" />
+                android:protectionLevel="internal|role" />
 
     <!-- Allows an app to prevent non-system-overlay windows from being drawn on top of it -->
     <permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"
@@ -4335,6 +4333,11 @@
     <permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"
                 android:protectionLevel="normal" />
 
+    <!-- Allows an application to deliver companion messages to system
+         -->
+    <permission android:name="android.permission.DELIVER_COMPANION_MESSAGES"
+                android:protectionLevel="normal" />
+
     <!-- Allows an application to create new companion device associations.
          @SystemApi
          @hide -->
diff --git a/core/res/res/drawable-nodpi/default_wallpaper.png b/core/res/res/drawable-nodpi/default_wallpaper.png
index 490ebee..5152972 100644
--- a/core/res/res/drawable-nodpi/default_wallpaper.png
+++ b/core/res/res/drawable-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
new file mode 100644
index 0000000..26376fb
--- /dev/null
+++ b/core/res/res/drawable-sw600dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
new file mode 100644
index 0000000..490ebee
--- /dev/null
+++ b/core/res/res/drawable-sw720dp-nodpi/default_wallpaper.png
Binary files differ
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index fc9b55f..755938e 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1221,7 +1221,8 @@
     <attr name="isFeatureSplit" format="boolean" />
 
     <!-- Flag to specify if this APK requires at least one split [either feature or
-         resource] to be present in order to function. Default value is false. -->
+         resource] to be present in order to function. Default value is false.
+         @deprecated Use {@link android.R.attr#requiredSplitTypes} instead. -->
     <attr name="isSplitRequired" format="boolean" />
 
     <!-- List of split types required by this APK to be present in order to function properly,
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b3ba1ee..48d4b5b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4595,6 +4595,13 @@
     <!-- Indicates whether device has a power button fingerprint sensor. -->
     <bool name="config_is_powerbutton_fps" translatable="false" >false</bool>
 
+    <!-- When each intermediate UDFPS enroll stage ends, as a fraction of total progress. -->
+    <string-array name="config_udfps_enroll_stage_thresholds" translatable="false">
+        <item>0.25</item>
+        <item>0.5</item>
+        <item>0.75</item>
+    </string-array>
+
     <!-- Messages that should not be shown to the user during face auth enrollment. This should be
          used to hide messages that may be too chatty or messages that the user can't do much about.
          Entries are defined in android.hardware.biometrics.face@1.0 types.hal -->
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 7e6ea7a..a6e42bd 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -2905,6 +2905,7 @@
     <public type="attr" name="usesNonSdkApi" id="0x0101058e" />
     <public type="attr" name="nonInteractiveUiTimeout" id="0x0101058f" />
     <public type="attr" name="isLightTheme" id="0x01010590" />
+    <!-- {@deprecated Use requiredSplitTypes instead.} -->
     <public type="attr" name="isSplitRequired" id="0x01010591" />
     <public type="attr" name="textLocale" id="0x01010592" />
     <public type="attr" name="settingsSliceUri" id="0x01010593" />
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b146e3f3..34e4ce8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2615,6 +2615,7 @@
   <java-symbol type="array" name="config_sfps_sensor_props" />
   <java-symbol type="integer" name="config_udfps_illumination_transition_ms" />
   <java-symbol type="bool" name="config_is_powerbutton_fps" />
+  <java-symbol type="array" name="config_udfps_enroll_stage_thresholds" />
 
   <java-symbol type="array" name="config_face_acquire_enroll_ignorelist" />
   <java-symbol type="array" name="config_face_acquire_vendor_enroll_ignorelist" />
@@ -4433,7 +4434,7 @@
   <java-symbol type="string" name="view_and_control_notification_title" />
   <java-symbol type="string" name="view_and_control_notification_content" />
   <java-symbol type="array" name="config_accessibility_allowed_install_source" />
-  
+
   <!-- Translation -->
   <java-symbol type="string" name="ui_translation_accessibility_translated_text" />
   <java-symbol type="string" name="ui_translation_accessibility_translation_finished" />
diff --git a/core/tests/coretests/assets/scroll_capture_test.html b/core/tests/coretests/assets/scroll_capture_test.html
new file mode 100644
index 0000000..5523887
--- /dev/null
+++ b/core/tests/coretests/assets/scroll_capture_test.html
@@ -0,0 +1,51 @@
+<!--
+  ~ 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.
+  -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>Sample Content</title>
+    <meta charset="UTF-8">
+    <style>
+       body { margin: 0px; }
+       div {
+         display: block;
+         width: 800px;
+         height: 300px;
+         color: white;
+         font-size: 20px;
+       }
+       #a { background-color: #ef476f; }
+       #b { background-color: #ffd166; }
+       #c { background-color: #06d6a0; }
+       #d { background-color: #118ab2; }
+       #e { background-color: #073b4c; }
+    </style>
+</head>
+<body>
+<div id="a">Item #1</div>
+<div id="b">Item #2</div>
+<div id="c">Item #3</div>
+<div id="d">Item #4</div>
+<div id="e">Item #5</div>
+<div id="a">Item #6</div>
+<div id="b">Item #7</div>
+<div id="c">Item #8</div>
+<div id="d">Item #9</div>
+<div id="e">Item #10</div>
+<div id="a">Item #11</div>
+<div id="b">Item #12</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
index ddb6729..4b19391 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureContextTest.java
@@ -39,9 +39,10 @@
     public void testConstructorAdditionalFlags() {
         final ComponentName componentName = new ComponentName("component", "name");
         final IBinder token = new Binder();
+        final IBinder windowToken = new Binder();
         final ContentCaptureContext ctx = new ContentCaptureContext(/* clientContext= */ null,
                 new ActivityId(/* taskId= */ 666, token), componentName, /* displayId= */
-                42, /* flags= */ 1);
+                42, windowToken, /* flags= */ 1);
         final ContentCaptureContext newCtx = new ContentCaptureContext(ctx, /* extraFlags= */ 2);
         assertThat(newCtx.getFlags()).isEqualTo(3);
         assertThat(newCtx.getActivityComponent()).isEqualTo(componentName);
@@ -50,6 +51,7 @@
         assertThat(activityId.getTaskId()).isEqualTo(666);
         assertThat(activityId.getToken()).isEqualTo(token);
         assertThat(newCtx.getDisplayId()).isEqualTo(42);
+        assertThat(newCtx.getWindowToken()).isEqualTo(windowToken);
         assertThat(newCtx.getExtras()).isNull();
         assertThat(newCtx.getLocusId()).isNull();
         assertThat(newCtx.getParentSessionId()).isNull();
diff --git a/core/tests/coretests/src/com/android/internal/view/AbsCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/AbsCaptureHelperTest.java
new file mode 100644
index 0000000..42b2b16
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/view/AbsCaptureHelperTest.java
@@ -0,0 +1,339 @@
+/*
+ * 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.internal.view;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test contains a set of operations designed to verify the behavior of a
+ * ScrollCaptureViewHelper implementation. Subclasses define and initialize
+ * the View hierarchy by overriding {@link #createScrollableContent} and provide the
+ * helper instance by overriding {@link #createHelper()}.
+ *
+ * @param <T> The concrete View subclass handled by the helper
+ * @param <H> The helper being tested
+ */
+public abstract class AbsCaptureHelperTest<T extends View, H extends ScrollCaptureViewHelper<T>> {
+
+    private static final String TAG = "AbsCaptureHelperTest";
+
+    static final int WINDOW_WIDTH = 800;
+    static final int WINDOW_HEIGHT = 1200;
+
+    static final int CAPTURE_HEIGHT = WINDOW_HEIGHT / 2;
+    static final int CONTENT_HEIGHT = WINDOW_HEIGHT * 3;
+
+    public static final int[] ITEM_COLORS = {
+            0xef476f,
+            0xffd166,
+            0x06d6a0,
+            0x118ab2,
+            0x073b4c
+    };
+
+    enum ScrollPosition {
+        /** Scroll to top edge */
+        TOP,
+        /** Scroll middle of content to the top edge of the window */
+        MIDDLE,
+        /** Scroll bottom edge of content to the bottom edge of the window. */
+        BOTTOM
+    }
+
+    private WindowManager mWm;
+    private FrameLayout mContentRoot;
+    private T mTarget;
+    private Rect mScrollBounds;
+    private H mHelper;
+
+    private Instrumentation mInstrumentation;
+
+    @Before
+    public final void createWindow() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        Context context = mInstrumentation.getTargetContext();
+        mWm = context.getSystemService(WindowManager.class);
+
+        // Instantiate parent view on the main thread
+        mInstrumentation.runOnMainSync(() -> mContentRoot = new FrameLayout(context));
+
+        // Called this directly on the test thread so it can block until loaded if needed
+        mTarget = createScrollableContent(mContentRoot);
+
+        // Finish constructing the window on the UI thread
+        mInstrumentation.runOnMainSync(() -> {
+            mContentRoot.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+            WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams(
+                    WINDOW_WIDTH,
+                    WINDOW_HEIGHT,
+                    TYPE_APPLICATION_OVERLAY,
+                    FLAG_NOT_TOUCHABLE,
+                    PixelFormat.OPAQUE);
+
+            windowLayoutParams.setTitle("ScrollCaptureHelperTest");
+            windowLayoutParams.gravity = Gravity.CENTER;
+            mWm.addView(mContentRoot, windowLayoutParams);
+        });
+    }
+
+    /**
+     * Create and prepare the instance of the helper under test.
+     *
+     * @return a new instance of ScrollCaptureViewHelper
+     */
+    protected abstract H createHelper();
+
+    /**
+     * Create a view/hierarchy containing a scrollable view to control. There should be zero
+     * padding or margins, and the test cases expect the scrollable content to be
+     * {@link #CONTENT_HEIGHT} px tall.
+     *
+     * @param parent the parent viewgroup holding the view to test
+     * @return an instance of T to test
+     */
+    @UiThread
+    protected abstract T createScrollableContent(ViewGroup parent);
+
+    /**
+     * Manually adjust the position of the scrollable view as setup for test scenarios.
+     *
+     * @param target the view target to adjust scroll position
+     * @param position the position to scroll to
+     */
+    @UiThread
+    protected void setInitialScrollPosition(T target, ScrollPosition position) {
+        Log.d(TAG, "scrollToPosition: " + position);
+        switch (position) {
+            case MIDDLE:
+                target.scrollBy(0, WINDOW_HEIGHT);
+                break;
+            case BOTTOM:
+                target.scrollBy(0, WINDOW_HEIGHT * 2);
+                break;
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromTop() {
+        initHelper(ScrollPosition.TOP);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, WINDOW_WIDTH, 0);
+        ScrollResult result = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertEmpty(result.availableArea);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromTop() {
+        initHelper(ScrollPosition.TOP);
+        Rect request = new Rect(0, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT + CAPTURE_HEIGHT);
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        assertThat(scrollResult.availableArea).isEqualTo(request);
+        // Capture height centered in the window
+        assertThat(scrollResult.scrollDelta).isEqualTo((WINDOW_HEIGHT / 2) + (CAPTURE_HEIGHT / 2));
+        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromMiddle() {
+        initHelper(ScrollPosition.MIDDLE);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, WINDOW_WIDTH, 0);
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        assertThat(scrollResult.availableArea).isEqualTo(request);
+        assertThat(scrollResult.scrollDelta).isEqualTo(
+                -CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
+        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromMiddle() {
+        initHelper(ScrollPosition.MIDDLE);
+
+        Rect request = new Rect(0, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT + CAPTURE_HEIGHT);
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        assertThat(scrollResult.availableArea).isEqualTo(request);
+        assertThat(scrollResult.scrollDelta).isEqualTo(
+                CAPTURE_HEIGHT + (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
+        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_up_fromBottom() {
+        initHelper(ScrollPosition.BOTTOM);
+
+        Rect request = new Rect(0, -CAPTURE_HEIGHT, WINDOW_WIDTH, 0);
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        assertThat(scrollResult.availableArea).isEqualTo(request);
+        assertThat(scrollResult.scrollDelta).isEqualTo(
+                -CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
+        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_down_fromBottom() {
+        initHelper(ScrollPosition.BOTTOM);
+
+        Rect request = new Rect(0, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT + CAPTURE_HEIGHT);
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        // The result is an empty rectangle and no scrolling, since it
+        // is not possible to physically scroll further down to make the
+        // requested area visible at all (it doesn't exist).
+        assertEmpty(scrollResult.availableArea);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_offTopEdge() {
+        initHelper(ScrollPosition.TOP);
+
+        int top = 0;
+        Rect request =
+                new Rect(0, top - (CAPTURE_HEIGHT / 2), WINDOW_WIDTH, top + (CAPTURE_HEIGHT / 2));
+
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        // The result is a partial result
+        Rect expectedResult = new Rect(request);
+        expectedResult.top += (CAPTURE_HEIGHT / 2); // top half clipped
+        assertThat(scrollResult.requestedArea).isEqualTo(request);
+        assertThat(scrollResult.availableArea).isEqualTo(expectedResult);
+        assertThat(scrollResult.scrollDelta).isEqualTo(0);
+        assertAvailableAreaPartiallyVisible(scrollResult, mTarget);
+    }
+
+    @Test
+    @UiThreadTest
+    public void onScrollRequested_offBottomEdge() {
+        initHelper(ScrollPosition.BOTTOM);
+
+        Rect request = new Rect(0, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT + CAPTURE_HEIGHT);
+        request.offset(0, -(CAPTURE_HEIGHT / 2));
+
+        ScrollResult scrollResult = requestScrollSync(mHelper, mScrollBounds, request);
+
+        Rect expectedResult = new Rect(request);
+        expectedResult.bottom -= 300; // bottom half clipped
+        assertThat(scrollResult.availableArea).isEqualTo(expectedResult);
+        assertThat(scrollResult.scrollDelta).isEqualTo(0);
+        assertAvailableAreaPartiallyVisible(scrollResult, mTarget);
+    }
+
+    @After
+    public final void removeWindow() throws InterruptedException {
+        mInstrumentation.runOnMainSync(() -> {
+            if (mContentRoot != null && mContentRoot.isAttachedToWindow()) {
+                mWm.removeViewImmediate(mContentRoot);
+            }
+        });
+    }
+
+    private void initHelper(ScrollPosition position) {
+        setInitialScrollPosition(mTarget, position);
+        mHelper = createHelper();
+        mScrollBounds = mHelper.onComputeScrollBounds(mTarget);
+        mHelper.onPrepareForStart(mTarget, mScrollBounds);
+    }
+
+    @NonNull
+    private ScrollResult requestScrollSync(H helper, Rect scrollBounds, Rect request) {
+        helper.onPrepareForStart(mTarget, scrollBounds);
+        ScrollResult result = helper.onScrollRequested(mTarget, scrollBounds, request);
+
+        assertNotNull(result);
+        return result;
+    }
+
+    static void assertEmpty(Rect r) {
+        if (r != null && !r.isEmpty()) {
+            fail("Not true that " + r + " is empty");
+        }
+    }
+
+    /**
+     * Returns the bounds of the view which are visible (not clipped).
+     */
+    static Rect getVisibleBounds(View v) {
+        Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
+        v.getLocalVisibleRect(r);
+        r.offset(-v.getScrollX(), -v.getScrollY());
+        return r;
+    }
+
+    static void assertAvailableAreaCompletelyVisible(ScrollResult result, View container) {
+        Rect localAvailable = new Rect(result.availableArea);
+        localAvailable.offset(0, -result.scrollDelta); // make relative to view top
+        Rect visibleBounds = getVisibleBounds(container);
+        if (!visibleBounds.contains(localAvailable)) {
+            fail("Not true that all of " + localAvailable + " is contained by " + visibleBounds);
+        }
+    }
+
+    static void assertAvailableAreaPartiallyVisible(ScrollResult result, View container) {
+        Rect requested = new Rect(result.availableArea);
+        requested.offset(0, -result.scrollDelta); // make relative
+        Rect localVisible = getVisibleBounds(container);
+        if (!Rect.intersects(localVisible, requested)) {
+            fail("Not true that any of " + requested + " is contained by " + localVisible);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/view/ListViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ListViewCaptureHelperTest.java
new file mode 100644
index 0000000..191574a
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/view/ListViewCaptureHelperTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.internal.view;
+
+import android.annotation.UiThread;
+import android.graphics.Color;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class ListViewCaptureHelperTest
+        extends AbsCaptureHelperTest<ListView, ListViewCaptureHelper> {
+
+    static final int CHILD_VIEW_HEIGHT = 300;
+    static final int CHILD_VIEW_COUNT = CONTENT_HEIGHT / CHILD_VIEW_HEIGHT;
+
+    @Override
+    protected ListViewCaptureHelper createHelper() {
+        return new ListViewCaptureHelper();
+    }
+
+    @Override
+    protected ListView createScrollableContent(ViewGroup parent) {
+        ListView listView = new ListView(parent.getContext());
+        listView.setDivider(null);
+        listView.setAdapter(new TestAdapter());
+        return listView;
+    }
+
+    @UiThread
+    protected void setInitialScrollPosition(ListView target, ScrollPosition position) {
+        int offset = 0;
+        switch (position) {
+            case MIDDLE:
+                offset = WINDOW_HEIGHT;
+                break;
+            case BOTTOM:
+                offset = WINDOW_HEIGHT * 2;
+                break;
+        }
+        int verticalPadding = target.getPaddingTop() + target.getPaddingBottom();
+        int step = (target.getHeight() - verticalPadding) / 2;
+        // ListView#scrollListBy will not scroll more than one screen height per call
+        while (offset > step) {
+            target.scrollListBy(step);
+            offset -= step;
+        }
+        target.scrollListBy(offset);
+    }
+
+    static final class TestAdapter extends BaseAdapter {
+
+        @Override
+        public int getCount() {
+            return CHILD_VIEW_COUNT;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return position;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return 0;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            TextView view = (convertView != null)
+                    ? (TextView) convertView : new TextView(parent.getContext());
+            view.setText("Item #" + position);
+            view.setTextColor(Color.WHITE);
+            view.setTextSize(20f);
+            view.setBackgroundColor(ITEM_COLORS[position % ITEM_COLORS.length]);
+            view.setMinHeight(CHILD_VIEW_HEIGHT);
+            return view;
+        }
+    }
+
+}
diff --git a/core/tests/coretests/src/com/android/internal/view/RecyclerViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/RecyclerViewCaptureHelperTest.java
index a6b26be..1df3168 100644
--- a/core/tests/coretests/src/com/android/internal/view/RecyclerViewCaptureHelperTest.java
+++ b/core/tests/coretests/src/com/android/internal/view/RecyclerViewCaptureHelperTest.java
@@ -16,273 +16,52 @@
 
 package com.android.internal.view;
 
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.view.Gravity;
+import android.annotation.UiThread;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
 import android.widget.TextView;
 
-import androidx.test.annotation.UiThreadTest;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;
 import com.android.internal.widget.LinearLayoutManager;
 import com.android.internal.widget.RecyclerView;
 
-import com.google.common.truth.Truth;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.Random;
 
 @RunWith(AndroidJUnit4.class)
-public class RecyclerViewCaptureHelperTest {
-    private static final int CHILD_VIEWS = 12;
+public class RecyclerViewCaptureHelperTest
+        extends AbsCaptureHelperTest<ViewGroup, RecyclerViewCaptureHelper> {
+
     private static final int CHILD_VIEW_HEIGHT = 300;
-    private static final int WINDOW_WIDTH = 800;
-    private static final int WINDOW_HEIGHT = 1200;
-    private static final int CAPTURE_HEIGHT = 600;
+    private static final int CHILD_VIEWS = CONTENT_HEIGHT / CHILD_VIEW_HEIGHT;
 
-    private FrameLayout mParent;
-    private RecyclerView mTarget;
-    private WindowManager mWm;
-
-    private WindowManager.LayoutParams mWindowLayoutParams;
-
-    private Context mContext;
-    private float mDensity;
-    private LinearLayoutManager mLinearLayoutManager;
-    private Instrumentation mInstrumentation;
-
-    @Before
-    @UiThreadTest
-    public void setUp() {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mContext = mInstrumentation.getContext();
-        mDensity = mContext.getResources().getDisplayMetrics().density;
-
-        mParent = new FrameLayout(mContext);
-
-        mTarget = new RecyclerView(mContext);
-        mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
-
-        mTarget.setAdapter(new TestAdapter());
-        mLinearLayoutManager =
-                new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false);
-        mTarget.setLayoutManager(mLinearLayoutManager);
-        mWm = mContext.getSystemService(WindowManager.class);
-
-        // Setup the window that we are going to use
-        mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT,
-                TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE);
-        mWindowLayoutParams.setTitle("ScrollViewCaptureHelper");
-        mWindowLayoutParams.gravity = Gravity.CENTER;
-        mWm.addView(mParent, mWindowLayoutParams);
+    @Override
+    protected RecyclerViewCaptureHelper createHelper() {
+        return new RecyclerViewCaptureHelper();
     }
 
-    @After
-    @UiThreadTest
-    public void tearDown() {
-        mWm.removeViewImmediate(mParent);
+    @Override
+    protected RecyclerView createScrollableContent(ViewGroup parent) {
+        RecyclerView recyclerView = new RecyclerView(parent.getContext());
+        recyclerView.setAdapter(new TestAdapter());
+        LinearLayoutManager layoutManager =
+                new LinearLayoutManager(parent.getContext(), LinearLayoutManager.VERTICAL, false);
+        recyclerView.setLayoutManager(layoutManager);
+        return recyclerView;
     }
 
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromTop() {
-        mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
-        // mTarget.createSnapshot(new ViewDebug.HardwareCanvasProvider(), false);
-
-        RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
-        rvc.onPrepareForStart(mTarget, scrollBounds);
-
-        assertThat(scrollBounds.height()).isGreaterThan(CAPTURE_HEIGHT);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = rvc.onScrollRequested(mTarget,
-                scrollBounds, request);
-
-        // The result is an empty rectangle and no scrolling, since it
-        // is not possible to physically scroll further up to make the
-        // requested area visible at all (it doesn't exist).
-        assertEmpty(scrollResult.availableArea);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromTop() {
-        mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
-
-        RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
-        rvc.onPrepareForStart(mTarget, scrollBounds);
-
-        assertThat(scrollBounds.height()).isGreaterThan(CAPTURE_HEIGHT);
-
-        // Capture between y = +1200 to +1800 pixels BELOW current top
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = rvc.onScrollRequested(mTarget, scrollBounds, request);
-        assertThat(request).isEqualTo(scrollResult.requestedArea);
-        assertThat(request).isEqualTo(scrollResult.availableArea);
-        // Capture height centered in the window
-        assertThat(scrollResult.scrollDelta).isEqualTo(
-                CAPTURE_HEIGHT + (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
-        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromMiddle() {
-        mTarget.scrollBy(0, WINDOW_HEIGHT);
-
-        RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
-        helper.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
-        assertThat(request).isEqualTo(scrollResult.requestedArea);
-        assertThat(request).isEqualTo(scrollResult.availableArea);
-        assertThat(scrollResult.scrollDelta).isEqualTo(
-                -CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
-        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromMiddle() {
-        mTarget.scrollBy(0, WINDOW_HEIGHT);
-
-        RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
-        helper.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
-        assertThat(request).isEqualTo(scrollResult.requestedArea);
-        assertThat(request).isEqualTo(scrollResult.availableArea);
-        assertThat(scrollResult.scrollDelta).isEqualTo(
-                CAPTURE_HEIGHT + (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
-        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromBottom() {
-        mTarget.scrollBy(0, WINDOW_HEIGHT * 2);
-
-        RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
-        helper.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
-        assertThat(request).isEqualTo(scrollResult.requestedArea);
-        assertThat(request).isEqualTo(scrollResult.availableArea);
-        assertThat(scrollResult.scrollDelta).isEqualTo(
-                -CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2);
-        assertAvailableAreaCompletelyVisible(scrollResult, mTarget);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromBottom() {
-        mTarget.scrollBy(0, WINDOW_HEIGHT * 3);
-
-        RecyclerViewCaptureHelper rvc = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = rvc.onComputeScrollBounds(mTarget);
-        rvc.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = rvc.onScrollRequested(mTarget,
-                scrollBounds, request);
-        Truth.assertThat(request).isEqualTo(scrollResult.requestedArea);
-
-        // The result is an empty rectangle and no scrolling, since it
-        // is not possible to physically scroll further down to make the
-        // requested area visible at all (it doesn't exist).
-        assertEmpty(scrollResult.availableArea);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_offTopEdge() {
-        mTarget.scrollBy(0, -(WINDOW_HEIGHT * 3));
-
-        RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
-        helper.onPrepareForStart(mTarget, scrollBounds);
-
-        // Create a request which lands halfway off the top of the content
-        //from -1500 to -900, (starting at 1200 = -300 to +300 within the content)
-        int top = 0;
-        Rect request = new Rect(
-                0, top - (CAPTURE_HEIGHT / 2),
-                scrollBounds.width(), top + (CAPTURE_HEIGHT / 2));
-
-        ScrollResult scrollResult = helper.onScrollRequested(mTarget, scrollBounds, request);
-        assertThat(request).isEqualTo(scrollResult.requestedArea);
-
-        ScrollResult result = helper.onScrollRequested(mTarget, scrollBounds, request);
-        // The result is a partial result
-        Rect expectedResult = new Rect(request);
-        expectedResult.top += (CAPTURE_HEIGHT / 2); // top half clipped
-        assertThat(expectedResult).isEqualTo(result.availableArea);
-        assertThat(scrollResult.scrollDelta).isEqualTo(0);
-        assertAvailableAreaPartiallyVisible(scrollResult, mTarget);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_offBottomEdge() {
-        mTarget.scrollBy(0, WINDOW_HEIGHT * 2);
-
-        RecyclerViewCaptureHelper helper = new RecyclerViewCaptureHelper();
-        Rect scrollBounds = helper.onComputeScrollBounds(mTarget);
-        helper.onPrepareForStart(mTarget, scrollBounds);
-
-        // Create a request which lands halfway off the bottom of the content
-        //from 600 to to 1200, (starting at 2400 = 3000 to  3600 within the content)
-
-        int bottom = WINDOW_HEIGHT;
-        Rect request = new Rect(
-                0, bottom - (CAPTURE_HEIGHT / 2),
-                scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2));
-
-        ScrollResult result = helper.onScrollRequested(mTarget, scrollBounds, request);
-
-        Rect expectedResult = new Rect(request);
-        expectedResult.bottom -= 300; // bottom half clipped
-        assertThat(expectedResult).isEqualTo(result.availableArea);
-        assertThat(result.scrollDelta).isEqualTo(0);
-        assertAvailableAreaPartiallyVisible(result, mTarget);
+    @UiThread
+    protected void setInitialScrollPosition(ViewGroup target, ScrollPosition position) {
+        switch (position) {
+            case MIDDLE:
+                target.scrollBy(0, WINDOW_HEIGHT);
+                break;
+            case BOTTOM:
+                target.scrollBy(0, WINDOW_HEIGHT * 2);
+                break;
+        }
     }
 
     static final class TestViewHolder extends RecyclerView.ViewHolder {
@@ -302,11 +81,9 @@
         @Override
         public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
             TextView view = (TextView) holder.itemView;
-            view.setText("Child #" + position);
-            view.setTextColor(Color.WHITE);
+            view.setText("Item #" + position);
             view.setTextSize(30f);
-            view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(),
-                    mRandom.nextFloat()));
+            view.setBackgroundColor(ITEM_COLORS[position % ITEM_COLORS.length]);
             view.setMinHeight(CHILD_VIEW_HEIGHT);
         }
 
@@ -315,34 +92,4 @@
             return CHILD_VIEWS;
         }
     }
-
-    static void assertEmpty(Rect r) {
-        if (r != null && !r.isEmpty()) {
-            fail("Not true that " + r + " is empty");
-        }
-    }
-
-    static Rect getVisibleRect(View v) {
-        Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
-        v.getLocalVisibleRect(r);
-        return r;
-    }
-
-    static void assertAvailableAreaCompletelyVisible(ScrollResult result, View container) {
-        Rect requested = new Rect(result.availableArea);
-        requested.offset(0, -result.scrollDelta); // make relative
-        Rect localVisible = getVisibleRect(container);
-        if (!localVisible.contains(requested)) {
-            fail("Not true that all of " + requested + " is contained by " + localVisible);
-        }
-    }
-
-    static void assertAvailableAreaPartiallyVisible(ScrollResult result, View container) {
-        Rect requested = new Rect(result.availableArea);
-        requested.offset(0, -result.scrollDelta); // make relative
-        Rect localVisible = getVisibleRect(container);
-        if (!Rect.intersects(localVisible, requested)) {
-            fail("Not true that any of " + requested + " is contained by " + localVisible);
-        }
-    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollCaptureViewSupportTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollCaptureViewSupportTest.java
index cb72b2d..699008b 100644
--- a/core/tests/coretests/src/com/android/internal/view/ScrollCaptureViewSupportTest.java
+++ b/core/tests/coretests/src/com/android/internal/view/ScrollCaptureViewSupportTest.java
@@ -32,6 +32,11 @@
 
     ScrollCaptureViewHelper<View> mViewHelper = new ScrollCaptureViewHelper<View>() {
         @Override
+        public boolean onAcceptSession(@NonNull View view) {
+            return true;
+        }
+
+        @Override
         public void onPrepareForStart(@NonNull View view, @NonNull Rect scrollBounds) {
         }
 
diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
index fc02ea9..9b9c69b 100644
--- a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
+++ b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
@@ -18,347 +18,44 @@
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 import android.content.Context;
 import android.graphics.Color;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.view.Gravity;
-import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.internal.view.ScrollCaptureViewHelper.ScrollResult;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
 import java.util.Random;
 
-public class ScrollViewCaptureHelperTest {
+public class ScrollViewCaptureHelperTest
+        extends AbsCaptureHelperTest<ViewGroup, ScrollViewCaptureHelper> {
 
-    private FrameLayout mParent;
-    private ScrollView mTarget;
-    private LinearLayout mContent;
-    private WindowManager mWm;
+    private static final int CHILD_VIEW_HEIGHT = 300;
+    private static final int CHILD_VIEW_COUNT = CONTENT_HEIGHT / CHILD_VIEW_HEIGHT;
 
-    private WindowManager.LayoutParams mWindowLayoutParams;
+    private Random mRandom = new Random(0L);
 
-    private static final int CHILD_VIEWS = 12;
-    public static final int CHILD_VIEW_HEIGHT = 300;
+    @Override
+    protected ScrollViewCaptureHelper createHelper() {
+        return new ScrollViewCaptureHelper();
+    }
 
-    private static final int WINDOW_WIDTH = 800;
-    private static final int WINDOW_HEIGHT = 1200;
-
-    private static final int CAPTURE_HEIGHT = 600;
-
-    private Random mRandom;
-
-    private Context mContext;
-    private float mDensity;
-
-    @Before
-    @UiThreadTest
-    public void setUp() {
-        mContext = InstrumentationRegistry.getInstrumentation().getContext();
-        mDensity = mContext.getResources().getDisplayMetrics().density;
-
-        mRandom = new Random();
-        mParent = new FrameLayout(mContext);
-
-        mTarget = new ScrollView(mContext);
-        mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
-
-        mContent = new LinearLayout(mContext);
-        mContent.setOrientation(LinearLayout.VERTICAL);
-        mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
-
-        for (int i = 0; i < CHILD_VIEWS; i++) {
+    @Override
+    protected ScrollView createScrollableContent(ViewGroup parent) {
+        Context mContext = parent.getContext();
+        ScrollView scrollView = new ScrollView(mContext);
+        LinearLayout content = new LinearLayout(mContext);
+        content.setOrientation(LinearLayout.VERTICAL);
+        scrollView.addView(content, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+        for (int i = 0; i < CHILD_VIEW_COUNT; i++) {
             TextView view = new TextView(mContext);
-            view.setText("Child #" + i);
+            view.setText("Item #" + i);
             view.setTextColor(Color.WHITE);
             view.setTextSize(30f);
-            view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(),
-                    mRandom.nextFloat()));
-            mContent.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT));
+            view.setBackgroundColor(ITEM_COLORS[i % ITEM_COLORS.length]);
+            content.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT));
         }
-
-        // Window -> Parent -> Target -> Content
-
-        mWm = mContext.getSystemService(WindowManager.class);
-
-        // Setup the window that we are going to use
-        mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT,
-                TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE);
-        mWindowLayoutParams.setTitle("ScrollViewCaptureHelper");
-        mWindowLayoutParams.gravity = Gravity.CENTER;
-        mWm.addView(mParent, mWindowLayoutParams);
-    }
-
-    @After
-    @UiThreadTest
-    public void tearDown() {
-        mWm.removeViewImmediate(mParent);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onPrepareForStart() {
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromTop() {
-        final int startScrollY = assertScrollToY(mTarget, 0);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget,
-                scrollBounds, request);
-
-        // The result is an empty rectangle and no scrolling, since it
-        // is not possible to physically scroll further up to make the
-        // requested area visible at all (it doesn't exist).
-        assertEmpty(scrollResult.availableArea);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromTop() {
-        final int startScrollY = assertScrollToY(mTarget, 0);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        assertTrue(scrollBounds.height() > CAPTURE_HEIGHT);
-
-        // Capture between y = +1200 to +1500 pixels BELOW current top
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-        assertRectEquals(request, scrollResult.availableArea);
-        assertRequestedRectCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(CAPTURE_HEIGHT + (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2,
-                scrollResult.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromMiddle() {
-        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-        assertRectEquals(request, scrollResult.availableArea);
-        assertRequestedRectCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(-CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2,
-                scrollResult.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromMiddle() {
-        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-        assertRectEquals(request, scrollResult.availableArea);
-        assertRequestedRectCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(CAPTURE_HEIGHT + (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2,
-                scrollResult.scrollDelta);
-
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_up_fromBottom() {
-        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-        assertRectEquals(request, scrollResult.availableArea);
-        assertRequestedRectCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(-CAPTURE_HEIGHT - (WINDOW_HEIGHT - CAPTURE_HEIGHT) / 2,
-                scrollResult.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_down_fromBottom() {
-        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(),
-                WINDOW_HEIGHT + CAPTURE_HEIGHT);
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-
-        // The result is an empty rectangle and no scrolling, since it
-        // is not possible to physically scroll further down to make the
-        // requested area visible at all (it doesn't exist).
-        assertEmpty(scrollResult.availableArea);
-        assertEquals(0, scrollResult.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_offTopEdge() {
-        final int startScrollY = assertScrollToY(mTarget, 0);
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        // Create a request which lands halfway off the top of the content
-        //from -1500 to -900, (starting at 1200 = -300 to +300 within the content)
-        int top = 0;
-        Rect request = new Rect(
-                0, top - (CAPTURE_HEIGHT / 2),
-                scrollBounds.width(), top + (CAPTURE_HEIGHT / 2));
-
-        ScrollResult scrollResult = svc.onScrollRequested(mTarget, scrollBounds, request);
-        assertRectEquals(request, scrollResult.requestedArea);
-
-        ScrollResult result = svc.onScrollRequested(mTarget, scrollBounds, request);
-        // The result is a partial result
-        Rect expectedResult = new Rect(request);
-        expectedResult.top += 300; // top half clipped
-        assertRectEquals(expectedResult, result.availableArea);
-        assertRequestedRectPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(0, scrollResult.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onScrollRequested_offBottomEdge() {
-        final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); // 2400
-
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        Rect scrollBounds = svc.onComputeScrollBounds(mTarget);
-        svc.onPrepareForStart(mTarget, scrollBounds);
-
-        // Create a request which lands halfway off the bottom of the content
-        //from 600 to to 1200, (starting at 2400 = 3000 to  3600 within the content)
-
-        int bottom = WINDOW_HEIGHT;
-        Rect request = new Rect(
-                0, bottom - (CAPTURE_HEIGHT / 2),
-                scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2));
-
-        ScrollResult result = svc.onScrollRequested(mTarget, scrollBounds, request);
-
-        Rect expectedResult = new Rect(request);
-        expectedResult.bottom -= 300; // bottom half clipped
-        assertRectEquals(expectedResult, result.availableArea);
-        assertRequestedRectPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
-        assertEquals(0, result.scrollDelta);
-    }
-
-    @Test
-    @UiThreadTest
-    public void onPrepareForEnd() {
-        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
-        svc.onPrepareForEnd(mTarget);
-    }
-
-
-    static void assertEmpty(Rect r) {
-        if (r != null && !r.isEmpty()) {
-            fail("Not true that " + r + " is empty");
-        }
-    }
-
-    static void assertContains(Rect parent, Rect child) {
-        if (!parent.contains(child)) {
-            fail("Not true that " + parent + " contains " + child);
-        }
-    }
-
-    static void assertRectEquals(Rect parent, Rect child) {
-        if (!parent.equals(child)) {
-            fail("Not true that " + parent + " is equal to " + child);
-        }
-    }
-
-    static Rect getVisibleRect(View v) {
-        Rect r = new Rect(0, 0, v.getWidth(), v.getHeight());
-        v.getLocalVisibleRect(r);
-        return r;
-    }
-
-
-    static int assertScrollToY(View v, int scrollY) {
-        v.scrollTo(0, scrollY);
-        int dest = v.getScrollY();
-        assertEquals(scrollY, dest);
-        return scrollY;
-    }
-
-    static void assertRequestedRectCompletelyVisible(int startScrollY, Rect requestRect,
-            Rect localVisibleNow) {
-        Rect captured = new Rect(localVisibleNow);
-        captured.offset(0, -startScrollY); // make relative
-
-        if (!captured.contains(requestRect)) {
-            fail("Not true that all of " + requestRect + " is contained by " + captured);
-        }
-    }
-    static void assertRequestedRectPartiallyVisible(int startScrollY, Rect requestRect,
-            Rect localVisibleNow) {
-        Rect captured = new Rect(localVisibleNow);
-        captured.offset(0, -startScrollY); // make relative
-
-        if (!Rect.intersects(captured, requestRect)) {
-            fail("Not true that any of " + requestRect + " intersects " + captured);
-        }
+        return scrollView;
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/view/WebViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/WebViewCaptureHelperTest.java
new file mode 100644
index 0000000..f38e21a
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/view/WebViewCaptureHelperTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.internal.view;
+
+import android.annotation.UiThread;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.util.Log;
+import android.view.ViewGroup;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebView.VisualStateCallback;
+import android.webkit.WebViewClient;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class WebViewCaptureHelperTest
+        extends AbsCaptureHelperTest<WebView, WebViewCaptureHelper> {
+
+    private static final String TAG = "WebViewCaptureHelperTest";
+
+    private WebView mWebView;
+
+    @Override
+    protected WebViewCaptureHelper createHelper() {
+        return new WebViewCaptureHelper();
+    }
+
+    @UiThread
+    protected void setInitialScrollPosition(WebView target, ScrollPosition position) {
+        int contentHeight = (int) (target.getContentHeight() * target.getScale());
+        int scrollBy = 0;
+        switch (position) {
+            case MIDDLE:
+                scrollBy =  WINDOW_HEIGHT;
+                break;
+            case BOTTOM:
+                scrollBy = WINDOW_HEIGHT * 2;
+                break;
+        }
+        Log.d(TAG, "scrollToPosition: position=" + position + " contentHeight=" + contentHeight
+                + " scrollBy=" + scrollBy);
+        target.scrollBy(0, scrollBy);
+    }
+
+    @Override
+    protected WebView createScrollableContent(ViewGroup parent) {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        CountDownLatch loaded = new CountDownLatch(1);
+        CountDownLatch scaleAdjusted = new CountDownLatch(1);
+        instrumentation.runOnMainSync(() -> {
+            Context mContext = parent.getContext();
+            mWebView = new WebView(mContext);
+            mWebView.setWebViewClient(new WebViewClient() {
+                @Override
+                public void onPageFinished(WebView view, String url) {
+                    Log.d(TAG, "onPageFinished: " + url);
+                }
+
+                @Override
+                public void onScaleChanged(WebView view, float oldScale, float newScale) {
+                    Log.d(TAG, "onScaleChanged: oldScale=" + oldScale + " newScale=" + newScale);
+                    // WebView reports 1.00125 when 1.0 is requested!?
+                    if (newScale > 0.99f && newScale < 1.01f) {
+                        scaleAdjusted.countDown();
+                    }
+                    Log.d(TAG, "scaleAdjusted: " + scaleAdjusted.getCount());
+                }
+
+                @Override
+                public void onLoadResource(WebView view, String url) {
+                    Log.d(TAG, "onLoadResource: " + url);
+                }
+
+                @Override
+                public void onPageCommitVisible(WebView view, String url) {
+                    Log.d(TAG, "onPageCommitVisible: " + url);
+                }
+            });
+
+            WebSettings webSettings = mWebView.getSettings();
+            webSettings.setAllowFileAccessFromFileURLs(true);
+            webSettings.setAllowUniversalAccessFromFileURLs(true);
+
+            mWebView.loadUrl("file:///android_asset/scroll_capture_test.html");
+            mWebView.postVisualStateCallback(1L, new VisualStateCallback() {
+                @Override
+                public void onComplete(long requestId) {
+                    Log.d(TAG, "VisualStateCallback::complete");
+                    loaded.countDown();
+                }
+            });
+        });
+
+        waitFor(loaded, 5, TimeUnit.SECONDS);
+
+        // Request a 1.0 zoom factor.
+        instrumentation.runOnMainSync(() -> mWebView.zoomBy(1.0f / mWebView.getScale()));
+        try {
+            // Wait for the scale factor to adjust.
+            //
+            // WebViewClient#onScaleChanged occasionally fails to fire causing a false
+            // negative test failure. WebView#getScale is not consistent across threads.
+            // So no attempt can be made here to wait for or verify the scale value directly,
+            // we just must wait and trust it changes to 1.0. :-(
+            //
+            Thread.sleep(300);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return mWebView;
+    }
+
+    private static boolean waitFor(CountDownLatch latch, long time, TimeUnit unit) {
+        try {
+            return latch.await(time, unit);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 07389b2..c250039 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -374,8 +374,6 @@
         <permission name="android.permission.STOP_APP_SWITCHES"/>
         <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
         <permission name="android.permission.SUSPEND_APPS" />
-        <!-- Permission required for UiModeManager and Telecom car mode CTS tests -->
-        <permission name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
         <permission name="android.permission.USE_RESERVED_DISK"/>
         <permission name="android.permission.UWB_PRIVILEGED"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
index e6d32ff..06df9568 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
@@ -42,6 +42,9 @@
     <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
     <!-- ATM.removeRootTasksWithActivityTypes() -->
     <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" />
+    <!-- Enable bubble notification-->
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
+
     <!-- Allow the test to write directly to /sdcard/ -->
     <application android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
new file mode 100644
index 0000000..d66a6e7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.wm.shell.flicker.bubble
+
+import android.app.INotificationManager
+import android.app.Instrumentation
+import android.app.NotificationManager
+import android.content.Context
+import android.os.ServiceManager
+import android.view.Surface
+import androidx.test.filters.FlakyTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerBuilderProvider
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.repetitions
+import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+/**
+ * Base configurations for Bubble flicker tests
+ */
+abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) {
+
+    protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    protected val context: Context = instrumentation.context
+    protected val testApp = LaunchBubbleHelper(instrumentation)
+
+    protected val notifyManager = INotificationManager.Stub.asInterface(
+            ServiceManager.getService(Context.NOTIFICATION_SERVICE))
+
+    protected val packageManager = context.getPackageManager()
+    protected val uid = packageManager.getApplicationInfo(
+            testApp.component.packageName, 0).uid
+
+    protected lateinit var addBubbleBtn: UiObject2
+
+    protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+
+    @JvmOverloads
+    protected open fun buildTransition(
+        extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {}
+    ): FlickerBuilder.(Map<String, Any?>) -> Unit {
+        return { configuration ->
+
+            setup {
+                test {
+                    notifyManager.setBubblesAllowed(testApp.component.packageName,
+                            uid, NotificationManager.BUBBLE_PREFERENCE_ALL)
+                    testApp.launchViaIntent(wmHelper)
+                    addBubbleBtn = device.wait(Until.findObject(
+                            By.text("Add Bubble")), FIND_OBJECT_TIMEOUT)
+                }
+            }
+
+            teardown {
+                notifyManager.setBubblesAllowed(testApp.component.packageName,
+                        uid, NotificationManager.BUBBLE_PREFERENCE_NONE)
+                testApp.exit()
+            }
+
+            extraSpec(this, configuration)
+        }
+    }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
+
+    @FlickerBuilderProvider
+    fun buildFlicker(): FlickerBuilder {
+        return FlickerBuilder(instrumentation).apply {
+            repeat { testSpec.config.repetitions }
+            transition(this, testSpec.config)
+        }
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance()
+                    .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0),
+                            repetitions = 5)
+        }
+
+        const val FIND_OBJECT_TIMEOUT = 2000L
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
new file mode 100644
index 0000000..42eeadf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.wm.shell.flicker.bubble
+
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen`
+ *
+ * Actions:
+ *     Launch an app and enable app's bubble notification
+ *     Send a bubble notification
+ *     The activity for the bubble is launched
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            setup {
+                test {
+                    addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found")
+                }
+            }
+            transitions {
+                val showBubble = device.wait(Until.findObject(
+                        By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
+                showBubble?.run { showBubble.click() } ?: error("Bubble notify not found")
+                device.pressBack()
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
new file mode 100644
index 0000000..47e8c0c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.wm.shell.flicker.bubble
+
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test creating a bubble notification
+ *
+ * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen`
+ *
+ * Actions:
+ *     Launch an app and enable app's bubble notification
+ *     Send a bubble notification
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            transitions {
+                addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found")
+            }
+        }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt
new file mode 100644
index 0000000..6695c17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.wm.shell.flicker.helpers
+
+import android.app.Instrumentation
+import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.wm.shell.flicker.testapp.Components
+
+class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper(
+    instrumentation,
+    Components.LaunchBubbleActivity.LABEL,
+    Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent()
+) {
+
+    companion object {
+        const val TEST_REPETITIONS = 1
+        const val TIMEOUT_MS = 3_000L
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
index 5549330..2cdbffa 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
@@ -107,5 +107,20 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity
+            android:name=".LaunchBubbleActivity"
+            android:label="LaunchBubbleApp"
+            android:exported="true"
+            android:launchMode="singleTop">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.VIEW" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".BubbleActivity"
+            android:label="BubbleApp"
+            android:exported="false"
+            android:resizeableActivity="true" />
     </application>
 </manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
new file mode 100644
index 0000000..d424a17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png
Binary files differ
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
new file mode 100644
index 0000000..b43f31d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/>
+</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
new file mode 100644
index 0000000..0e8c7a0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z"
+      android:fillColor="#000000"
+      android:fillType="evenOdd"/>
+</vector>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
new file mode 100644
index 0000000..f8b0ca3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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="match_parent">
+    <Button
+        android:id="@+id/button_finish"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:layout_marginStart="8dp"
+        android:text="Finish" />
+    <Button
+        android:id="@+id/button_new_task"
+        android:layout_width="wrap_content"
+        android:layout_height="46dp"
+        android:layout_marginStart="8dp"
+        android:text="New Task" />
+    <Button
+        android:id="@+id/button_new_bubble"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginEnd="8dp"
+        android:text="New Bubble" />
+
+    <Button
+        android:id="@+id/button_activity_for_result"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginStart="8dp"
+        android:text="Activity For Result" />
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..5c7b18e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black">
+
+        <Button
+            android:id="@+id/button_create"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:text="Add Bubble" />
+</FrameLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
new file mode 100644
index 0000000..bc3bc75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java
@@ -0,0 +1,77 @@
+/*
+ * 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.wm.shell.flicker.testapp;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Toast;
+
+public class BubbleActivity extends Activity {
+    private int mNotifId = 0;
+
+    public BubbleActivity() {
+        super();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Intent intent = getIntent();
+        if (intent != null) {
+            mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1);
+        } else {
+            mNotifId = -1;
+        }
+
+        setContentView(R.layout.activity_bubble);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED";
+        Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
new file mode 100644
index 0000000..d72c8d5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
@@ -0,0 +1,154 @@
+/*
+ * 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.wm.shell.flicker.testapp;
+
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.drawable.Icon;
+import android.os.SystemClock;
+import android.view.WindowManager;
+
+import java.util.HashMap;
+
+public class BubbleHelper {
+
+    static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID";
+    static final String CHANNEL_ID = "bubbles";
+    static final String CHANNEL_NAME = "Bubbles";
+    static final int DEFAULT_HEIGHT_DP = 300;
+
+    private static BubbleHelper sInstance;
+
+    private final Context mContext;
+    private NotificationManager mNotificationManager;
+    private float mDisplayHeight;
+
+    private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>();
+
+    private int mNextNotifyId = 0;
+    private int mColourIndex = 0;
+
+    public static class BubbleInfo {
+        public int id;
+        public int height;
+        public Icon icon;
+
+        public BubbleInfo(int id, int height, Icon icon) {
+            this.id = id;
+            this.height = height;
+            this.icon = icon;
+        }
+    }
+
+    public static BubbleHelper getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new BubbleHelper(context);
+        }
+        return sInstance;
+    }
+
+    private BubbleHelper(Context context) {
+        mContext = context;
+        mNotificationManager = context.getSystemService(NotificationManager.class);
+
+        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
+                NotificationManager.IMPORTANCE_DEFAULT);
+        channel.setDescription("Channel that posts bubbles");
+        channel.setAllowBubbles(true);
+        mNotificationManager.createNotificationChannel(channel);
+
+        Point p = new Point();
+        WindowManager wm = context.getSystemService(WindowManager.class);
+        wm.getDefaultDisplay().getRealSize(p);
+        mDisplayHeight = p.y;
+
+    }
+
+      private int getNextNotifyId() {
+        int id = mNextNotifyId;
+        mNextNotifyId++;
+        return id;
+    }
+
+    private Icon getIcon() {
+        return Icon.createWithResource(mContext, R.drawable.bg);
+    }
+
+    public int addNewBubble(boolean autoExpand, boolean suppressNotif) {
+        int id = getNextNotifyId();
+        BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon());
+        mBubbleMap.put(info.id, info);
+
+        Notification.BubbleMetadata data = getBubbleBuilder(info)
+                .setSuppressNotification(suppressNotif)
+                .setAutoExpandBubble(false)
+                .build();
+        Notification notification = getNotificationBuilder(info.id)
+                .setBubbleMetadata(data).build();
+
+        mNotificationManager.notify(info.id, notification);
+        return info.id;
+    }
+
+    private Notification.Builder getNotificationBuilder(int id) {
+        Person chatBot = new Person.Builder()
+                .setBot(true)
+                .setName("BubbleBot")
+                .setImportant(true)
+                .build();
+
+        RemoteInput remoteInput = new RemoteInput.Builder("key")
+                .setLabel("Reply")
+                .build();
+
+        String shortcutId = "BubbleChat";
+        return new Notification.Builder(mContext, CHANNEL_ID)
+                .setChannelId(CHANNEL_ID)
+                .setShortcutId(shortcutId)
+                .setContentIntent(PendingIntent.getActivity(mContext, 0,
+                        new Intent(mContext, LaunchBubbleActivity.class),
+                        PendingIntent.FLAG_UPDATE_CURRENT))
+                .setStyle(new Notification.MessagingStyle(chatBot)
+                        .setConversationTitle("Bubble Chat")
+                        .addMessage("Hello? This is bubble: " + id,
+                                SystemClock.currentThreadTimeMillis() - 300000, chatBot)
+                        .addMessage("Is it me, " + id + ", you're looking for?",
+                                SystemClock.currentThreadTimeMillis(), chatBot)
+                )
+                .setSmallIcon(R.drawable.ic_bubble);
+    }
+
+    private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) {
+        Intent target = new Intent(mContext, BubbleActivity.class);
+        target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id);
+        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+
+        return new Notification.BubbleMetadata.Builder()
+                .setIntent(bubbleIntent)
+                .setIcon(info.icon)
+                .setDesiredHeight(info.height);
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
index 0ead91b..0ed59bd 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
@@ -87,4 +87,16 @@
         public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
                 PACKAGE_NAME + ".SplitScreenSecondaryActivity");
     }
+
+    public static class LaunchBubbleActivity {
+        public static final String LABEL = "LaunchBubbleApp";
+        public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+                PACKAGE_NAME + ".LaunchBubbleActivity");
+    }
+
+    public static class BubbleActivity {
+        public static final String LABEL = "BubbleApp";
+        public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+                PACKAGE_NAME + ".BubbleActivity");
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
new file mode 100644
index 0000000..c55f9d7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.wm.shell.flicker.testapp;
+
+
+import android.app.Activity;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+
+import java.util.Arrays;
+
+public class LaunchBubbleActivity extends Activity {
+
+    private BubbleHelper mBubbleHelper;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        addInboxShortcut(getApplicationContext());
+        mBubbleHelper = BubbleHelper.getInstance(this);
+        setContentView(R.layout.activity_main);
+        findViewById(R.id.button_create).setOnClickListener(this::add);
+    }
+
+    private void add(View v) {
+        mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */);
+    }
+
+    private void addInboxShortcut(Context context) {
+        Icon icon = Icon.createWithResource(this, R.drawable.bg);
+        Person[] persons = new Person[4];
+        for (int i = 0; i < persons.length; i++) {
+            persons[i] = new Person.Builder()
+                    .setBot(false)
+                    .setIcon(icon)
+                    .setName("google" + i)
+                    .setImportant(true)
+                    .build();
+        }
+
+        ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat")
+                .setShortLabel("BubbleChat")
+                .setLongLived(true)
+                .setIntent(new Intent(Intent.ACTION_VIEW))
+                .setIcon(Icon.createWithResource(context, R.drawable.ic_message))
+                .setPersons(persons)
+                .build();
+        ShortcutManager scmanager = context.getSystemService(ShortcutManager.class);
+        scmanager.addDynamicShortcuts(Arrays.asList(shortcut));
+    }
+
+}
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index f116641..983c776 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -269,7 +269,7 @@
 
 void ClipPath::draw(SkCanvas* outCanvas, bool useStagingData) {
     SkPath tempStagingPath;
-    outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath));
+    outCanvas->clipPath(getUpdatedPath(useStagingData, &tempStagingPath), true);
 }
 
 Group::Group(const Group& group) : Node(group) {
diff --git a/media/aidl/android/media/audio/common/AudioDeviceDescription.aidl b/media/aidl/android/media/audio/common/AudioDeviceDescription.aidl
index 4e8a735..c21acca 100644
--- a/media/aidl/android/media/audio/common/AudioDeviceDescription.aidl
+++ b/media/aidl/android/media/audio/common/AudioDeviceDescription.aidl
@@ -39,7 +39,8 @@
      * Usually it's some kind of a communication protocol, e.g. Bluetooth SCO or
      * USB. There is a list of connection types recognized by the framework,
      * defined using 'CONNECTION_' constants. Vendors can add their own
-     * connection types with "vx.<vendor>." prefix.
+     * connection types with "VX_<vendor>_" prefix, where the "vendor" part
+     * must consist of at least 3 letters or numbers.
      *
      * When the 'connection' field is left empty and 'type != NONE | DEFAULT',
      * it is assumed that the device is permanently attached to the audio
@@ -50,14 +51,11 @@
      */
     @utf8InCpp String connection;
     /**
-     * Analog connection, for example, via 3.5 mm analog jack.
+     * Analog connection, for example, via 3.5 mm analog jack,
+     * or a low-end (analog) desk dock.
      */
     const @utf8InCpp String CONNECTION_ANALOG = "analog";
     /**
-     * Low-End (Analog) Desk Dock.
-     */
-    const @utf8InCpp String CONNECTION_ANALOG_DOCK = "analog-dock";
-    /**
      * Bluetooth A2DP connection.
      */
     const @utf8InCpp String CONNECTION_BT_A2DP = "bt-a2dp";
@@ -74,10 +72,6 @@
      */
     const @utf8InCpp String CONNECTION_BUS = "bus";
     /**
-     * High-End (Digital) Desk Dock.
-     */
-    const @utf8InCpp String CONNECTION_DIGITAL_DOCK = "digital-dock";
-    /**
      * HDMI connection.
      */
     const @utf8InCpp String CONNECTION_HDMI = "hdmi";
@@ -102,7 +96,7 @@
      */
     const @utf8InCpp String CONNECTION_WIRELESS = "wireless";
     /**
-     * USB connection.
+     * USB connection. The Android device is the USB Host.
      */
     const @utf8InCpp String CONNECTION_USB = "usb";
 }
diff --git a/media/aidl/android/media/audio/common/AudioDeviceType.aidl b/media/aidl/android/media/audio/common/AudioDeviceType.aidl
index 95dbe2a..afe6d10 100644
--- a/media/aidl/android/media/audio/common/AudioDeviceType.aidl
+++ b/media/aidl/android/media/audio/common/AudioDeviceType.aidl
@@ -47,6 +47,7 @@
     IN_DEFAULT = 1,
     /**
      * A device implementing Android Open Accessory protocol.
+     * Note: AOAv2 audio support has been deprecated in Android 8.0.
      */
     IN_ACCESSORY = 2,
     /**
@@ -94,12 +95,17 @@
      */
     IN_TV_TUNER = 13,
     /**
+     * Input from a phone / table dock.
+     */
+    IN_DOCK = 14,
+    /**
      * The "default" device is used when the client does not have any
      * preference for a particular device.
      */
     OUT_DEFAULT = 129,
     /**
      * A device implementing Android Open Accessory protocol.
+     * Note: AOAv2 audio support has been deprecated in Android 8.0.
      */
     OUT_ACCESSORY = 130,
     /**
@@ -158,4 +164,8 @@
      * Output into a telephone line.
      */
     OUT_TELEPHONY_TX = 144,
+    /**
+     * Output into a speaker of a phone / table dock.
+     */
+    OUT_DOCK = 145,
 }
diff --git a/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceDescription.aidl b/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceDescription.aidl
index b4d71d7..1c66a8f 100644
--- a/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceDescription.aidl
+++ b/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceDescription.aidl
@@ -38,12 +38,10 @@
   android.media.audio.common.AudioDeviceType type = android.media.audio.common.AudioDeviceType.NONE;
   @utf8InCpp String connection;
   const @utf8InCpp String CONNECTION_ANALOG = "analog";
-  const @utf8InCpp String CONNECTION_ANALOG_DOCK = "analog-dock";
   const @utf8InCpp String CONNECTION_BT_A2DP = "bt-a2dp";
   const @utf8InCpp String CONNECTION_BT_LE = "bt-le";
   const @utf8InCpp String CONNECTION_BT_SCO = "bt-sco";
   const @utf8InCpp String CONNECTION_BUS = "bus";
-  const @utf8InCpp String CONNECTION_DIGITAL_DOCK = "digital-dock";
   const @utf8InCpp String CONNECTION_HDMI = "hdmi";
   const @utf8InCpp String CONNECTION_HDMI_ARC = "hdmi-arc";
   const @utf8InCpp String CONNECTION_HDMI_EARC = "hdmi-earc";
diff --git a/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceType.aidl b/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceType.aidl
index ffdb778..0b7b77c 100644
--- a/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceType.aidl
+++ b/media/aidl_api/android.media.audio.common.types/current/android/media/audio/common/AudioDeviceType.aidl
@@ -49,6 +49,7 @@
   IN_SUBMIX = 11,
   IN_TELEPHONY_RX = 12,
   IN_TV_TUNER = 13,
+  IN_DOCK = 14,
   OUT_DEFAULT = 129,
   OUT_ACCESSORY = 130,
   OUT_AFE_PROXY = 131,
@@ -65,4 +66,5 @@
   OUT_SPEAKER_SAFE = 142,
   OUT_SUBMIX = 143,
   OUT_TELEPHONY_TX = 144,
+  OUT_DOCK = 145,
 }
diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java
index 3b835f7..8b1624b 100644
--- a/media/java/android/media/Spatializer.java
+++ b/media/java/android/media/Spatializer.java
@@ -129,6 +129,14 @@
      */
     public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1;
 
+    /**
+     * @hide
+     * Constant indicating the {@code Spatializer} on this device supports the spatialization of
+     * multichannel bed plus objects.
+     * @see #getImmersiveAudioLevel()
+     */
+    public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2;
+
     /** @hide */
     @IntDef(flag = false, value = {
             HEAD_TRACKING_MODE_UNSUPPORTED,
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index a2ae5023..e854b02 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -104,4 +104,11 @@
          allow it to use the whole screen space, 0.6 will allow it to use just under half of the
          screen. -->
     <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
+
+    <!-- The actual amount of translation that is applied to the bouncer when it animates from one
+         side of the screen to the other in one-handed mode. Note that it will always translate from
+         the side of the screen to the other (it will "jump" closer to the destination while the
+         opacity is zero), but this controls how much motion will actually be applied to it while
+         animating. Larger values will cause it to move "faster" while fading out/in. -->
+    <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 6729f3d..0386217 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -57,5 +57,5 @@
 
     <bool name="flag_combined_status_bar_signal_icons">false</bool>
 
-    <bool name="flag_new_user_switcher">false</bool>
+    <bool name="flag_new_user_switcher">true</bool>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 64d214d..a5b18ca 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -21,8 +21,7 @@
 
 import static java.lang.Integer.max;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -40,6 +39,8 @@
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
 import androidx.annotation.VisibleForTesting;
@@ -55,7 +56,6 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.shared.system.SysUiStatsLog;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -85,6 +85,13 @@
 
     private static final long IME_DISAPPEAR_DURATION_MS = 125;
 
+    // The duration of the animation to switch bouncer sides.
+    private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+
+    // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+    // remainder will fade it back in again.
+    private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
     private AlertDialog mAlertDialog;
@@ -322,18 +329,87 @@
                 ? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth());
 
         if (animate) {
-            mRunningOneHandedAnimator =
-                    mSecurityViewFlipper.animate().translationX(targetTranslation);
-            mRunningOneHandedAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mRunningOneHandedAnimator.setListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mRunningOneHandedAnimator = null;
+            // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out
+            // at the same time. The issue is, the bouncer should only move a short amount (120dp or
+            // so), but obviously needs to go from one side of the screen to the other. This needs a
+            // pretty custom animation.
+            //
+            // This works as follows. It uses a ValueAnimation to simply drive the animation
+            // progress. This animator is responsible for both the translation of the bouncer, and
+            // the current fade. It will fade the bouncer out while also moving it along the 120dp
+            // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer
+            // to its destination, then fade it back in again. The effect is that the bouncer will
+            // move from 0 -> X while fading out, then (destination - X) -> destination while fading
+            // back in again.
+            // TODO(b/195012405): Make this animation properly abortable.
+            Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.fast_out_extra_slow_in);
+            Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+            Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+            ValueAnimator anim = ValueAnimator.ofFloat(0.0f, 1.0f);
+            anim.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
+            anim.setInterpolator(Interpolators.LINEAR);
+
+            int initialTranslation = (int) mSecurityViewFlipper.getTranslationX();
+            int totalTranslation = (int) getResources().getDimension(
+                    R.dimen.one_handed_bouncer_move_animation_translation);
+
+            final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering()
+                    && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+            if (shouldRestoreLayerType) {
+                mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+            }
+
+            anim.addUpdateListener(animation -> {
+                float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
+                boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
+
+                int currentTranslation = (int) (positionInterpolator.getInterpolation(
+                        animation.getAnimatedFraction()) * totalTranslation);
+                int translationRemaining = totalTranslation - currentTranslation;
+
+                // Flip the sign if we're going from right to left.
+                if (mIsSecurityViewLeftAligned) {
+                    currentTranslation = -currentTranslation;
+                    translationRemaining = -translationRemaining;
+                }
+
+                if (isFadingOut) {
+                    // The bouncer fades out over the first X%.
+                    float fadeOutFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */0.0f,
+                            /* rangeMax= */1.0f,
+                            /* valueMin= */0.0f,
+                            /* valueMax= */switchPoint,
+                            animation.getAnimatedFraction());
+                    float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+                    mSecurityViewFlipper.setAlpha(1f - opacity);
+
+                    // Animate away from the source.
+                    mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+                } else {
+                    // And in again over the remaining (100-X)%.
+                    float fadeInFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */0.0f,
+                            /* rangeMax= */1.0f,
+                            /* valueMin= */switchPoint,
+                            /* valueMax= */1.0f,
+                            animation.getAnimatedFraction());
+
+                    float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+                    mSecurityViewFlipper.setAlpha(opacity);
+
+                    // Fading back in, animate towards the destination.
+                    mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+                }
+
+                if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+                    mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
                 }
             });
 
-            mRunningOneHandedAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-            mRunningOneHandedAnimator.start();
+            anim.start();
         } else {
             mSecurityViewFlipper.setTranslationX(targetTranslation);
         }
@@ -682,4 +758,3 @@
         mDisappearAnimRunning = false;
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
index 17818cd..59d9aff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java
@@ -19,8 +19,9 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.util.MathUtils.constrain;
 import static android.util.MathUtils.sq;
+import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static java.util.Objects.requireNonNull;
 
@@ -41,7 +42,6 @@
 import android.graphics.drawable.LayerDrawable;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -95,7 +95,6 @@
     private boolean mIsShowing;
     private boolean mIsDownInEnlargedTouchArea;
     private boolean mIsDragging = false;
-    private boolean mImeVisibility;
     @Alignment
     private int mAlignment;
     @SizeType
@@ -108,8 +107,10 @@
     private int mRadiusType;
     private int mMargin;
     private int mPadding;
-    private int mScreenHeight;
-    private int mScreenWidth;
+    // The display width excludes the window insets of the system bar and display cutout.
+    private int mDisplayHeight;
+    // The display Height excludes the window insets of the system bar and display cutout.
+    private int mDisplayWidth;
     private int mIconWidth;
     private int mIconHeight;
     private int mInset;
@@ -118,6 +119,8 @@
     private int mRelativeToPointerDownX;
     private int mRelativeToPointerDownY;
     private float mRadius;
+    private final Rect mDisplayInsetsRect = new Rect();
+    private final Rect mImeInsetsRect = new Rect();
     private final Position mPosition;
     private float mSquareScaledTouchSlop;
     private final Configuration mLastConfiguration;
@@ -506,9 +509,21 @@
     }
 
     private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
-        final boolean currentImeVisibility = insets.isVisible(ime());
-        if (currentImeVisibility != mImeVisibility) {
-            mImeVisibility = currentImeVisibility;
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final Rect displayWindowInsetsRect = getDisplayInsets(windowMetrics).toRect();
+        if (!displayWindowInsetsRect.equals(mDisplayInsetsRect)) {
+            updateDisplaySizeWith(windowMetrics);
+            updateLocationWith(mPosition);
+        }
+
+        final Rect imeInsetsRect = windowMetrics.getWindowInsets().getInsets(ime()).toRect();
+        if (!imeInsetsRect.equals(mImeInsetsRect)) {
+            if (isImeVisible(imeInsetsRect)) {
+                mImeInsetsRect.set(imeInsetsRect);
+            } else {
+                mImeInsetsRect.setEmpty();
+            }
+
             updateLocationWith(mPosition);
         }
 
@@ -520,6 +535,11 @@
                 || (side == Alignment.LEFT && downX > currentRawX);
     }
 
+    private boolean isImeVisible(Rect imeInsetsRect) {
+        return imeInsetsRect.left != 0 || imeInsetsRect.top != 0 || imeInsetsRect.right != 0
+                || imeInsetsRect.bottom != 0;
+    }
+
     private boolean hasExceededTouchSlop(int startX, int startY, int endX, int endY) {
         return (sq(endX - startX) + sq(endY - startY)) > mSquareScaledTouchSlop;
     }
@@ -546,9 +566,9 @@
 
     private void updateDimensions() {
         final Resources res = getResources();
-        final DisplayMetrics dm = res.getDisplayMetrics();
-        mScreenWidth = dm.widthPixels;
-        mScreenHeight = dm.heightPixels;
+
+        updateDisplaySizeWith(mWindowManager.getCurrentWindowMetrics());
+
         mMargin =
                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
         mInset =
@@ -560,6 +580,15 @@
         updateItemViewDimensionsWith(mSizeType);
     }
 
+    private void updateDisplaySizeWith(WindowMetrics metrics) {
+        final Rect displayBounds = metrics.getBounds();
+        final Insets displayInsets = getDisplayInsets(metrics);
+        mDisplayInsetsRect.set(displayInsets.toRect());
+        displayBounds.inset(displayInsets);
+        mDisplayWidth = displayBounds.width();
+        mDisplayHeight = displayBounds.height();
+    }
+
     private void updateItemViewDimensionsWith(@SizeType int sizeType) {
         final Resources res = getResources();
         final int paddingResId =
@@ -684,11 +713,11 @@
     }
 
     private int getMaxWindowX() {
-        return mScreenWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
+        return mDisplayWidth - getMarginStartEndWith(mLastConfiguration) - getLayoutWidth();
     }
 
     private int getMaxWindowY() {
-        return mScreenHeight - getWindowHeight();
+        return mDisplayHeight - getWindowHeight();
     }
 
     private InstantInsetLayerDrawable getMenuLayerDrawable() {
@@ -699,8 +728,13 @@
         return (GradientDrawable) getMenuLayerDrawable().getDrawable(INDEX_MENU_ITEM);
     }
 
+    private Insets getDisplayInsets(WindowMetrics metrics) {
+        return metrics.getWindowInsets().getInsetsIgnoringVisibility(
+                systemBars() | displayCutout());
+    }
+
     /**
-     * Updates the floating menu to be fixed at the side of the screen.
+     * Updates the floating menu to be fixed at the side of the display.
      */
     private void updateLocationWith(Position position) {
         final @Alignment int alignment = transformToAlignment(position.getPercentageX());
@@ -716,15 +750,9 @@
      * @return the moving interval if they overlap each other, otherwise 0.
      */
     private int getInterval() {
-        if (!mImeVisibility) {
-            return 0;
-        }
-
-        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
-        final Insets imeInsets = windowMetrics.getWindowInsets().getInsets(
-                ime() | navigationBars());
-        final int imeY = mScreenHeight - imeInsets.bottom;
-        final int layoutBottomY = mCurrentLayoutParams.y + getWindowHeight();
+        final int currentLayoutY = (int) (mPosition.getPercentageY() * getMaxWindowY());
+        final int imeY = mDisplayHeight - mImeInsetsRect.bottom;
+        final int layoutBottomY = currentLayoutY + getWindowHeight();
 
         return layoutBottomY > imeY ? (layoutBottomY - imeY) : 0;
     }
@@ -855,11 +883,12 @@
 
     @VisibleForTesting
     Rect getAvailableBounds() {
-        return new Rect(0, 0, mScreenWidth - getWindowWidth(), mScreenHeight - getWindowHeight());
+        return new Rect(0, 0, mDisplayWidth - getWindowWidth(),
+                mDisplayHeight - getWindowHeight());
     }
 
     private int getMaxLayoutHeight() {
-        return mScreenHeight - mMargin * 2;
+        return mDisplayHeight - mMargin * 2;
     }
 
     private int getLayoutWidth() {
@@ -875,7 +904,7 @@
     }
 
     private int getWindowHeight() {
-        return Math.min(mScreenHeight, mMargin * 2 + getLayoutHeight());
+        return Math.min(mDisplayHeight, mMargin * 2 + getLayoutHeight());
     }
 
     private void setSystemGestureExclusion() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 535f091..5018e57c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -244,7 +244,7 @@
                 final UdfpsEnrollHelper enrollHelper;
                 if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
                         || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) {
-                    enrollHelper = new UdfpsEnrollHelper(mContext, reason);
+                    enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason);
                 } else {
                     enrollHelper = null;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
index d407756..2034ff3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
@@ -16,9 +16,11 @@
 
 package com.android.systemui.biometrics;
 
+import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
@@ -26,11 +28,17 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.TypedValue;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.graphics.ColorUtils;
 import com.android.systemui.R;
 
 /**
@@ -39,10 +47,20 @@
 public class UdfpsEnrollDrawable extends UdfpsDrawable {
     private static final String TAG = "UdfpsAnimationEnroll";
 
-    private static final long ANIM_DURATION = 800;
+    private static final long HINT_COLOR_ANIM_DELAY_MS = 233L;
+    private static final long HINT_COLOR_ANIM_DURATION_MS = 517L;
+    private static final long HINT_WIDTH_ANIM_DURATION_MS = 233L;
+    private static final long TARGET_ANIM_DURATION_LONG = 800L;
+    private static final long TARGET_ANIM_DURATION_SHORT = 600L;
     // 1 + SCALE_MAX is the maximum that the moving target will animate to
     private static final float SCALE_MAX = 0.25f;
 
+    private static final float HINT_PADDING_DP = 10f;
+    private static final float HINT_MAX_WIDTH_DP = 6f;
+    private static final float HINT_ANGLE = 40f;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
     @NonNull private final Drawable mMovingTargetFpIcon;
     @NonNull private final Paint mSensorOutlinePaint;
     @NonNull private final Paint mBlueFill;
@@ -51,17 +69,41 @@
     @Nullable private UdfpsEnrollHelper mEnrollHelper;
 
     // Moving target animator set
-    @Nullable AnimatorSet mAnimatorSet;
+    @Nullable AnimatorSet mTargetAnimatorSet;
     // Moving target location
     float mCurrentX;
     float mCurrentY;
     // Moving target size
     float mCurrentScale = 1.f;
 
+    @ColorInt private final int mHintColorFaded;
+    @ColorInt private final int mHintColorHighlight;
+    private final float mHintMaxWidthPx;
+    private final float mHintPaddingPx;
+
+    @NonNull private final Animator.AnimatorListener mTargetAnimListener;
+
+    private boolean mShouldShowTipHint = false;
+    @NonNull private final Paint mTipHintPaint;
+    @Nullable private AnimatorSet mTipHintAnimatorSet;
+    @Nullable private ValueAnimator mTipHintColorAnimator;
+    @Nullable private ValueAnimator mTipHintWidthAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintColorUpdateListener;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mTipHintWidthUpdateListener;
+    @NonNull private final Animator.AnimatorListener mTipHintPulseListener;
+
+    private boolean mShouldShowEdgeHint = false;
+    @NonNull private final Paint mEdgeHintPaint;
+    @Nullable private AnimatorSet mEdgeHintAnimatorSet;
+    @Nullable private ValueAnimator mEdgeHintColorAnimator;
+    @Nullable private ValueAnimator mEdgeHintWidthAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintColorUpdateListener;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mEdgeHintWidthUpdateListener;
+    @NonNull private final Animator.AnimatorListener mEdgeHintPulseListener;
+
     UdfpsEnrollDrawable(@NonNull Context context) {
         super(context);
 
-
         mSensorOutlinePaint = new Paint(0 /* flags */);
         mSensorOutlinePaint.setAntiAlias(true);
         mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_enroll_icon));
@@ -78,6 +120,117 @@
         mMovingTargetFpIcon.mutate();
 
         mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon));
+
+        mHintColorFaded = getHintColorFaded(context);
+        mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress);
+        mHintMaxWidthPx = Utils.dpToPixels(context, HINT_MAX_WIDTH_DP);
+        mHintPaddingPx = Utils.dpToPixels(context, HINT_PADDING_DP);
+
+        mTargetAnimListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                updateTipHintVisibility();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+
+        mTipHintPaint = new Paint(0 /* flags */);
+        mTipHintPaint.setAntiAlias(true);
+        mTipHintPaint.setColor(mHintColorFaded);
+        mTipHintPaint.setStyle(Paint.Style.STROKE);
+        mTipHintPaint.setStrokeCap(Paint.Cap.ROUND);
+        mTipHintPaint.setStrokeWidth(0f);
+        mTipHintColorUpdateListener = animation -> {
+            mTipHintPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mTipHintWidthUpdateListener = animation -> {
+            mTipHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mTipHintPulseListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mHandler.postDelayed(() -> {
+                    mTipHintColorAnimator =
+                            ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorFaded);
+                    mTipHintColorAnimator.setInterpolator(new LinearInterpolator());
+                    mTipHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+                    mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+                    mTipHintColorAnimator.start();
+                }, HINT_COLOR_ANIM_DELAY_MS);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+
+        mEdgeHintPaint = new Paint(0 /* flags */);
+        mEdgeHintPaint.setAntiAlias(true);
+        mEdgeHintPaint.setColor(mHintColorFaded);
+        mEdgeHintPaint.setStyle(Paint.Style.STROKE);
+        mEdgeHintPaint.setStrokeCap(Paint.Cap.ROUND);
+        mEdgeHintPaint.setStrokeWidth(0f);
+        mEdgeHintColorUpdateListener = animation -> {
+            mEdgeHintPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mEdgeHintWidthUpdateListener = animation -> {
+            mEdgeHintPaint.setStrokeWidth((float) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+        mEdgeHintPulseListener = new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {}
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mHandler.postDelayed(() -> {
+                    mEdgeHintColorAnimator =
+                            ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorFaded);
+                    mEdgeHintColorAnimator.setInterpolator(new LinearInterpolator());
+                    mEdgeHintColorAnimator.setDuration(HINT_COLOR_ANIM_DURATION_MS);
+                    mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+                    mEdgeHintColorAnimator.start();
+                }, HINT_COLOR_ANIM_DELAY_MS);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {}
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {}
+        };
+    }
+
+    @ColorInt
+    private static int getHintColorFaded(@NonNull Context context) {
+        final TypedValue tv = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, tv, true);
+        final int alpha = (int) (tv.getFloat() * 255f);
+
+        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+        final TypedArray ta = context.obtainStyledAttributes(attrs);
+        try {
+            @ColorInt final int color = ta.getColor(0, context.getColor(R.color.white_disabled));
+            return ColorUtils.setAlphaComponent(color, alpha);
+        } finally {
+            ta.recycle();
+        }
     }
 
     void setEnrollHelper(@NonNull UdfpsEnrollHelper helper) {
@@ -98,41 +251,154 @@
     }
 
     void onEnrollmentProgress(int remaining, int totalSteps) {
-        if (mEnrollHelper.isCenterEnrollmentComplete()) {
-            if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
-                mAnimatorSet.end();
+        if (mEnrollHelper == null) {
+            return;
+        }
+
+        if (!mEnrollHelper.isCenterEnrollmentStage()) {
+            if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
+                mTargetAnimatorSet.end();
             }
 
             final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint();
+            if (mCurrentX != point.x || mCurrentY != point.y) {
+                final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
+                x.addUpdateListener(animation -> {
+                    mCurrentX = (float) animation.getAnimatedValue();
+                    invalidateSelf();
+                });
 
-            final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
-            x.addUpdateListener(animation -> {
-                mCurrentX = (float) animation.getAnimatedValue();
-                invalidateSelf();
-            });
+                final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
+                y.addUpdateListener(animation -> {
+                    mCurrentY = (float) animation.getAnimatedValue();
+                    invalidateSelf();
+                });
 
-            final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
-            y.addUpdateListener(animation -> {
-                mCurrentY = (float) animation.getAnimatedValue();
-                invalidateSelf();
-            });
+                final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
+                final long duration = isMovingToCenter
+                        ? TARGET_ANIM_DURATION_SHORT
+                        : TARGET_ANIM_DURATION_LONG;
 
-            final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
-            scale.setDuration(ANIM_DURATION);
-            scale.addUpdateListener(animation -> {
-                // Grow then shrink
-                mCurrentScale = 1 +
-                        SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
-                invalidateSelf();
-            });
+                final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
+                scale.setDuration(duration);
+                scale.addUpdateListener(animation -> {
+                    // Grow then shrink
+                    mCurrentScale = 1
+                            + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
+                    invalidateSelf();
+                });
 
-            mAnimatorSet = new AnimatorSet();
+                mTargetAnimatorSet = new AnimatorSet();
 
-            mAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
-            mAnimatorSet.setDuration(ANIM_DURATION);
-            mAnimatorSet.playTogether(x, y, scale);
-            mAnimatorSet.start();
+                mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+                mTargetAnimatorSet.setDuration(duration);
+                mTargetAnimatorSet.addListener(mTargetAnimListener);
+                mTargetAnimatorSet.playTogether(x, y, scale);
+                mTargetAnimatorSet.start();
+            } else {
+                updateTipHintVisibility();
+            }
+        } else {
+            updateTipHintVisibility();
         }
+
+        updateEdgeHintVisibility();
+    }
+
+    private void updateTipHintVisibility() {
+        final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage();
+        if (mShouldShowTipHint == shouldShow) {
+            return;
+        }
+        mShouldShowTipHint = shouldShow;
+
+        if (mTipHintWidthAnimator != null && mTipHintWidthAnimator.isRunning()) {
+            mTipHintWidthAnimator.cancel();
+        }
+
+        final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+        mTipHintWidthAnimator = ValueAnimator.ofFloat(mTipHintPaint.getStrokeWidth(), targetWidth);
+        mTipHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mTipHintWidthAnimator.addUpdateListener(mTipHintWidthUpdateListener);
+
+        if (shouldShow) {
+            startTipHintPulseAnimation();
+        } else {
+            mTipHintWidthAnimator.start();
+        }
+    }
+
+    private void updateEdgeHintVisibility() {
+        final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage();
+        if (mShouldShowEdgeHint == shouldShow) {
+            return;
+        }
+        mShouldShowEdgeHint = shouldShow;
+
+        if (mEdgeHintWidthAnimator != null && mEdgeHintWidthAnimator.isRunning()) {
+            mEdgeHintWidthAnimator.cancel();
+        }
+
+        final float targetWidth = shouldShow ? mHintMaxWidthPx : 0f;
+        mEdgeHintWidthAnimator =
+                ValueAnimator.ofFloat(mEdgeHintPaint.getStrokeWidth(), targetWidth);
+        mEdgeHintWidthAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mEdgeHintWidthAnimator.addUpdateListener(mEdgeHintWidthUpdateListener);
+
+        if (shouldShow) {
+            startEdgeHintPulseAnimation();
+        } else {
+            mEdgeHintWidthAnimator.start();
+        }
+    }
+
+    private void startTipHintPulseAnimation() {
+        mHandler.removeCallbacksAndMessages(null);
+        if (mTipHintAnimatorSet != null && mTipHintAnimatorSet.isRunning()) {
+            mTipHintAnimatorSet.cancel();
+        }
+        if (mTipHintColorAnimator != null && mTipHintColorAnimator.isRunning()) {
+            mTipHintColorAnimator.cancel();
+        }
+
+        mTipHintColorAnimator = ValueAnimator.ofArgb(mTipHintPaint.getColor(), mHintColorHighlight);
+        mTipHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mTipHintColorAnimator.addUpdateListener(mTipHintColorUpdateListener);
+        mTipHintColorAnimator.addListener(mTipHintPulseListener);
+
+        mTipHintAnimatorSet = new AnimatorSet();
+        mTipHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+        mTipHintAnimatorSet.playTogether(mTipHintColorAnimator, mTipHintWidthAnimator);
+        mTipHintAnimatorSet.start();
+    }
+
+    private void startEdgeHintPulseAnimation() {
+        mHandler.removeCallbacksAndMessages(null);
+        if (mEdgeHintAnimatorSet != null && mEdgeHintAnimatorSet.isRunning()) {
+            mEdgeHintAnimatorSet.cancel();
+        }
+        if (mEdgeHintColorAnimator != null && mEdgeHintColorAnimator.isRunning()) {
+            mEdgeHintColorAnimator.cancel();
+        }
+
+        mEdgeHintColorAnimator =
+                ValueAnimator.ofArgb(mEdgeHintPaint.getColor(), mHintColorHighlight);
+        mEdgeHintColorAnimator.setDuration(HINT_WIDTH_ANIM_DURATION_MS);
+        mEdgeHintColorAnimator.addUpdateListener(mEdgeHintColorUpdateListener);
+        mEdgeHintColorAnimator.addListener(mEdgeHintPulseListener);
+
+        mEdgeHintAnimatorSet = new AnimatorSet();
+        mEdgeHintAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
+        mEdgeHintAnimatorSet.playTogether(mEdgeHintColorAnimator, mEdgeHintWidthAnimator);
+        mEdgeHintAnimatorSet.start();
+    }
+
+    private boolean isTipHintVisible() {
+        return mTipHintPaint.getStrokeWidth() > 0f;
+    }
+
+    private boolean isEdgeHintVisible() {
+        return mEdgeHintPaint.getStrokeWidth() > 0f;
     }
 
     @Override
@@ -142,7 +408,7 @@
         }
 
         // Draw moving target
-        if (mEnrollHelper.isCenterEnrollmentComplete()) {
+        if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) {
             canvas.save();
             canvas.translate(mCurrentX, mCurrentY);
 
@@ -162,6 +428,59 @@
             mFingerprintDrawable.setAlpha(mAlpha);
             mSensorOutlinePaint.setAlpha(mAlpha);
         }
+
+        // Draw the finger tip or edges hint.
+        if (isTipHintVisible() || isEdgeHintVisible()) {
+            canvas.save();
+
+            // Make arcs start from the top, rather than the right.
+            canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+
+            final float halfSensorHeight = Math.abs(mSensorRect.bottom - mSensorRect.top) / 2f;
+            final float halfSensorWidth = Math.abs(mSensorRect.right - mSensorRect.left) / 2f;
+            final float hintXOffset = halfSensorWidth + mHintPaddingPx;
+            final float hintYOffset = halfSensorHeight + mHintPaddingPx;
+
+            if (isTipHintVisible()) {
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mTipHintPaint);
+            }
+
+            if (isEdgeHintVisible()) {
+                // Draw right edge hint.
+                canvas.rotate(-90f, mSensorRect.centerX(), mSensorRect.centerY());
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mEdgeHintPaint);
+
+                // Draw left edge hint.
+                canvas.rotate(180f, mSensorRect.centerX(), mSensorRect.centerY());
+                canvas.drawArc(
+                        mSensorRect.centerX() - hintXOffset,
+                        mSensorRect.centerY() - hintYOffset,
+                        mSensorRect.centerX() + hintXOffset,
+                        mSensorRect.centerY() + hintYOffset,
+                        -HINT_ANGLE / 2f,
+                        HINT_ANGLE,
+                        false /* useCenter */,
+                        mEdgeHintPaint);
+            }
+
+            canvas.restore();
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
index c6d2192..d5c763d3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollHelper.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.PointF;
 import android.hardware.biometrics.BiometricOverlayConstants;
+import android.hardware.fingerprint.FingerprintManager;
 import android.os.Build;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -44,16 +45,14 @@
     private static final String NEW_COORDS_OVERRIDE =
             "com.android.systemui.biometrics.UdfpsNewCoords";
 
-    // Enroll with two center touches before going to guided enrollment
-    private static final int NUM_CENTER_TOUCHES = 2;
-
     interface Listener {
         void onEnrollmentProgress(int remaining, int totalSteps);
+        void onEnrollmentHelp(int remaining, int totalSteps);
         void onLastStepAcquired();
-        void onEnrollmentHelp();
     }
 
     @NonNull private final Context mContext;
+    @NonNull private final FingerprintManager mFingerprintManager;
     // IUdfpsOverlayController reason
     private final int mEnrollReason;
     private final boolean mAccessibilityEnabled;
@@ -66,10 +65,15 @@
     // interface makes no promises about monotonically increasing by one each time.
     private int mLocationsEnrolled = 0;
 
+    private int mCenterTouchCount = 0;
+
     @Nullable Listener mListener;
 
-    public UdfpsEnrollHelper(@NonNull Context context, int reason) {
+    public UdfpsEnrollHelper(@NonNull Context context,
+            @NonNull FingerprintManager fingerprintManager, int reason) {
+
         mContext = context;
+        mFingerprintManager = fingerprintManager;
         mEnrollReason = reason;
 
         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
@@ -118,6 +122,14 @@
         }
     }
 
+    int getStageCount() {
+        return mFingerprintManager.getEnrollStageCount();
+    }
+
+    int getStageThresholdSteps(int totalSteps, int stageIndex) {
+        return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
+    }
+
     boolean shouldShowProgressBar() {
         return mEnrollReason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING;
     }
@@ -129,6 +141,9 @@
 
         if (remaining != mRemainingSteps) {
             mLocationsEnrolled++;
+            if (isCenterEnrollmentStage()) {
+                mCenterTouchCount++;
+            }
         }
 
         mRemainingSteps = remaining;
@@ -140,7 +155,7 @@
 
     void onEnrollmentHelp() {
         if (mListener != null) {
-            mListener.onEnrollmentHelp();
+            mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
         }
     }
 
@@ -155,19 +170,41 @@
         }
     }
 
-    boolean isCenterEnrollmentComplete() {
+    boolean isCenterEnrollmentStage() {
         if (mTotalSteps == -1 || mRemainingSteps == -1) {
-            return false;
-        } else if (mAccessibilityEnabled) {
+            return true;
+        }
+        return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
+    }
+
+    boolean isGuidedEnrollmentStage() {
+        if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
             return false;
         }
-        final int stepsEnrolled = mTotalSteps - mRemainingSteps;
-        return stepsEnrolled >= NUM_CENTER_TOUCHES;
+        final int progressSteps = mTotalSteps - mRemainingSteps;
+        return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
+                && progressSteps < getStageThresholdSteps(mTotalSteps, 1);
+    }
+
+    boolean isTipEnrollmentStage() {
+        if (mTotalSteps == -1 || mRemainingSteps == -1) {
+            return false;
+        }
+        final int progressSteps = mTotalSteps - mRemainingSteps;
+        return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
+                && progressSteps < getStageThresholdSteps(mTotalSteps, 2);
+    }
+
+    boolean isEdgeEnrollmentStage() {
+        if (mTotalSteps == -1 || mRemainingSteps == -1) {
+            return false;
+        }
+        return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
     }
 
     @NonNull
     PointF getNextGuidedEnrollmentPoint() {
-        if (mAccessibilityEnabled) {
+        if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
             return new PointF(0f, 0f);
         }
 
@@ -177,7 +214,7 @@
                     SCALE_OVERRIDE, SCALE,
                     UserHandle.USER_CURRENT);
         }
-        final int index = mLocationsEnrolled - NUM_CENTER_TOUCHES;
+        final int index = mLocationsEnrolled - mCenterTouchCount;
         final PointF originalPoint = mGuidedEnrollmentPoints
                 .get(index % mGuidedEnrollmentPoints.size());
         return new PointF(originalPoint.x * scale, originalPoint.y * scale);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 373d17c8..b2a5409 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -16,163 +16,129 @@
 
 package com.android.systemui.biometrics;
 
-import android.animation.ArgbEvaluator;
-import android.animation.ValueAnimator;
-import android.annotation.ColorInt;
 import android.content.Context;
-import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
-import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
-import android.util.TypedValue;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.systemui.R;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * UDFPS enrollment progress bar.
  */
 public class UdfpsEnrollProgressBarDrawable extends Drawable {
+    private static final String TAG = "UdfpsProgressBar";
 
-    private static final String TAG = "UdfpsEnrollProgressBarDrawable";
-
-    private static final float PROGRESS_BAR_THICKNESS_DP = 12;
+    private static final float SEGMENT_GAP_ANGLE = 12f;
 
     @NonNull private final Context mContext;
-    @NonNull private final Paint mBackgroundCirclePaint;
-    @NonNull private final Paint mProgressPaint;
 
-    @Nullable private ValueAnimator mProgressAnimator;
-    @Nullable private ValueAnimator mProgressShowingHelpAnimator;
-    @Nullable private ValueAnimator mProgressHidingHelpAnimator;
-    @ColorInt private final int mProgressColor;
-    @ColorInt private final int mProgressHelpColor;
-    private final int mShortAnimationDuration;
-    private float mProgress;
-    private int mRotation; // After last step, rotate the progress bar once
-    private boolean mLastStepAcquired;
+    @Nullable private UdfpsEnrollHelper mEnrollHelper;
+    @NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
+    private int mTotalSteps = 1;
+    private int mProgressSteps = 0;
+    private boolean mIsShowingHelp = false;
 
     public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
         mContext = context;
-
-        mShortAnimationDuration = context.getResources()
-                .getInteger(com.android.internal.R.integer.config_shortAnimTime);
-        mProgressColor = context.getColor(R.color.udfps_enroll_progress);
-        mProgressHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
-
-        mBackgroundCirclePaint = new Paint();
-        mBackgroundCirclePaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
-        mBackgroundCirclePaint.setColor(context.getColor(R.color.white_disabled));
-        mBackgroundCirclePaint.setAntiAlias(true);
-        mBackgroundCirclePaint.setStyle(Paint.Style.STROKE);
-
-        // Background circle color + alpha
-        TypedArray tc = context.obtainStyledAttributes(
-                new int[] {android.R.attr.colorControlNormal});
-        int tintColor = tc.getColor(0, mBackgroundCirclePaint.getColor());
-        mBackgroundCirclePaint.setColor(tintColor);
-        tc.recycle();
-        TypedValue alpha = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
-        mBackgroundCirclePaint.setAlpha((int) (alpha.getFloat() * 255));
-
-        // Progress should not be color extracted
-        mProgressPaint = new Paint();
-        mProgressPaint.setStrokeWidth(Utils.dpToPixels(context, PROGRESS_BAR_THICKNESS_DP));
-        mProgressPaint.setColor(mProgressColor);
-        mProgressPaint.setAntiAlias(true);
-        mProgressPaint.setStyle(Paint.Style.STROKE);
-        mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
     }
 
-    void setEnrollmentProgress(int remaining, int totalSteps) {
-        // Add one so that the first steps actually changes progress, but also so that the last
-        // step ends at 1.0
-        final float progress = (totalSteps - remaining + 1) / (float) (totalSteps + 1);
-        setEnrollmentProgress(progress);
-    }
-
-    private void setEnrollmentProgress(float progress) {
-        if (mLastStepAcquired) {
-            return;
-        }
-
-        long animationDuration = mShortAnimationDuration;
-
-        hideEnrollmentHelp();
-
-        if (progress == 1.f) {
-            animationDuration = 400;
-            final ValueAnimator rotationAnimator = ValueAnimator.ofInt(0, 400);
-            rotationAnimator.setDuration(animationDuration);
-            rotationAnimator.addUpdateListener(animation -> {
-                Log.d(TAG, "Rotation: " + mRotation);
-                mRotation = (int) animation.getAnimatedValue();
-                invalidateSelf();
-            });
-            rotationAnimator.start();
-        }
-
-        if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
-            mProgressAnimator.cancel();
-        }
-
-        mProgressAnimator = ValueAnimator.ofFloat(mProgress, progress);
-        mProgressAnimator.setDuration(animationDuration);
-        mProgressAnimator.addUpdateListener(animation -> {
-            mProgress = (float) animation.getAnimatedValue();
+    void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
+        mEnrollHelper = enrollHelper;
+        if (enrollHelper != null) {
+            final int stageCount = enrollHelper.getStageCount();
+            mSegments = new ArrayList<>(stageCount);
+            float startAngle = SEGMENT_GAP_ANGLE / 2f;
+            final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
+            final Runnable invalidateRunnable = this::invalidateSelf;
+            for (int index = 0; index < stageCount; index++) {
+                mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
+                        sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
+                startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
+            }
             invalidateSelf();
-        });
-        mProgressAnimator.start();
+        }
+    }
+
+    void onEnrollmentProgress(int remaining, int totalSteps) {
+        mTotalSteps = totalSteps;
+        updateState(getProgressSteps(remaining, totalSteps), false /* isShowingHelp */);
+    }
+
+    void onEnrollmentHelp(int remaining, int totalSteps) {
+        updateState(getProgressSteps(remaining, totalSteps), true /* isShowingHelp */);
     }
 
     void onLastStepAcquired() {
-        setEnrollmentProgress(1.f);
-        mLastStepAcquired = true;
+        updateState(mTotalSteps, false /* isShowingHelp */);
     }
 
-    void onEnrollmentHelp() {
-        if (mProgressShowingHelpAnimator != null || mProgressAnimator == null) {
-            return; // already showing or at 0% (no progress bar visible)
-        }
-
-        if (mProgressHidingHelpAnimator != null && mProgressHidingHelpAnimator.isRunning()) {
-            mProgressHidingHelpAnimator.cancel();
-        }
-        mProgressHidingHelpAnimator = null;
-
-        mProgressShowingHelpAnimator = getProgressColorAnimator(
-                mProgressPaint.getColor(), mProgressHelpColor);
-        mProgressShowingHelpAnimator.start();
+    private static int getProgressSteps(int remaining, int totalSteps) {
+        // Show some progress for the initial touch.
+        return Math.max(1, totalSteps - remaining);
     }
 
-    private void hideEnrollmentHelp() {
-        if (mProgressHidingHelpAnimator != null || mProgressShowingHelpAnimator == null) {
-            return; // already hidden or help never shown
-        }
-
-        if (mProgressShowingHelpAnimator != null && mProgressShowingHelpAnimator.isRunning()) {
-            mProgressShowingHelpAnimator.cancel();
-        }
-        mProgressShowingHelpAnimator = null;
-
-        mProgressHidingHelpAnimator = getProgressColorAnimator(
-                mProgressPaint.getColor(), mProgressColor);
-        mProgressHidingHelpAnimator.start();
+    private void updateState(int progressSteps, boolean isShowingHelp) {
+        updateProgress(progressSteps);
+        updateFillColor(isShowingHelp);
     }
 
-    private ValueAnimator getProgressColorAnimator(@ColorInt int from, @ColorInt int to) {
-        final ValueAnimator animator = ValueAnimator.ofObject(
-                ArgbEvaluator.getInstance(), from, to);
-        animator.setDuration(mShortAnimationDuration);
-        animator.addUpdateListener(animation -> {
-            mProgressPaint.setColor((int) animation.getAnimatedValue());
-        });
-        return animator;
+    private void updateProgress(int progressSteps) {
+        if (mProgressSteps == progressSteps) {
+            return;
+        }
+        mProgressSteps = progressSteps;
+
+        if (mEnrollHelper == null) {
+            Log.e(TAG, "updateState: UDFPS enroll helper was null");
+            return;
+        }
+
+        int index = 0;
+        int prevThreshold = 0;
+        while (index < mSegments.size()) {
+            final UdfpsEnrollProgressBarSegment segment = mSegments.get(index);
+            final int thresholdSteps = mEnrollHelper.getStageThresholdSteps(mTotalSteps, index);
+            if (progressSteps >= thresholdSteps && segment.getProgress() < 1f) {
+                segment.updateProgress(1f);
+                break;
+            } else if (progressSteps >= prevThreshold && progressSteps < thresholdSteps) {
+                final int relativeSteps = progressSteps - prevThreshold;
+                final int relativeThreshold = thresholdSteps - prevThreshold;
+                final float segmentProgress = (float) relativeSteps / (float) relativeThreshold;
+                segment.updateProgress(segmentProgress);
+                break;
+            }
+
+            index++;
+            prevThreshold = thresholdSteps;
+        }
+
+        if (progressSteps >= mTotalSteps) {
+            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+                segment.startCompletionAnimation();
+            }
+        } else {
+            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+                segment.cancelCompletionAnimation();
+            }
+        }
+    }
+
+    private void updateFillColor(boolean isShowingHelp) {
+        if (mIsShowingHelp == isShowingHelp) {
+            return;
+        }
+        mIsShowingHelp = isShowingHelp;
+
+        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+            segment.updateFillColor(isShowingHelp);
+        }
     }
 
     @Override
@@ -180,43 +146,22 @@
         canvas.save();
 
         // Progress starts from the top, instead of the right
-        canvas.rotate(-90 + mRotation, getBounds().centerX(), getBounds().centerY());
+        canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
 
-        // Progress bar "background track"
-        final float halfPaddingPx = Utils.dpToPixels(mContext, PROGRESS_BAR_THICKNESS_DP) / 2;
-        canvas.drawArc(halfPaddingPx,
-                halfPaddingPx,
-                getBounds().right - halfPaddingPx,
-                getBounds().bottom - halfPaddingPx,
-                0,
-                360,
-                false,
-                mBackgroundCirclePaint
-        );
-
-        final float progress = 360.f * mProgress;
-        // Progress
-        canvas.drawArc(halfPaddingPx,
-                halfPaddingPx,
-                getBounds().right - halfPaddingPx,
-                getBounds().bottom - halfPaddingPx,
-                0,
-                progress,
-                false,
-                mProgressPaint
-        );
+        // Draw each of the enroll segments.
+        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
+            segment.draw(canvas);
+        }
 
         canvas.restore();
     }
 
     @Override
     public void setAlpha(int alpha) {
-
     }
 
     @Override
     public void setColorFilter(@Nullable ColorFilter colorFilter) {
-
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
new file mode 100644
index 0000000..bd6ab44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
@@ -0,0 +1,280 @@
+/*
+ * 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.systemui.biometrics;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.TypedValue;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.R;
+
+/**
+ * A single segment of the UDFPS enrollment progress bar.
+ */
+public class UdfpsEnrollProgressBarSegment {
+    private static final String TAG = "UdfpsProgressBarSegment";
+
+    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
+    private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
+    private static final long OVER_SWEEP_ANIMATION_DELAY_MS = 200L;
+    private static final long OVER_SWEEP_ANIMATION_DURATION_MS = 200L;
+
+    private static final float STROKE_WIDTH_DP = 12f;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    @NonNull private final Rect mBounds;
+    @NonNull private final Runnable mInvalidateRunnable;
+    private final float mStartAngle;
+    private final float mSweepAngle;
+    private final float mMaxOverSweepAngle;
+    private final float mStrokeWidthPx;
+    @ColorInt private final int mProgressColor;
+    @ColorInt private final int mHelpColor;
+
+    @NonNull private final Paint mBackgroundPaint;
+    @NonNull private final Paint mProgressPaint;
+
+    private float mProgress = 0f;
+    private float mAnimatedProgress = 0f;
+    @Nullable private ValueAnimator mProgressAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
+
+    private boolean mIsShowingHelp = false;
+    @Nullable private ValueAnimator mFillColorAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
+
+    private float mOverSweepAngle = 0f;
+    @Nullable private ValueAnimator mOverSweepAnimator;
+    @Nullable private ValueAnimator mOverSweepReverseAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mOverSweepUpdateListener;
+    @NonNull private final Runnable mOverSweepAnimationRunnable;
+
+    public UdfpsEnrollProgressBarSegment(@NonNull Context context, @NonNull Rect bounds,
+            float startAngle, float sweepAngle, float maxOverSweepAngle,
+            @NonNull Runnable invalidateRunnable) {
+
+        mBounds = bounds;
+        mInvalidateRunnable = invalidateRunnable;
+        mStartAngle = startAngle;
+        mSweepAngle = sweepAngle;
+        mMaxOverSweepAngle = maxOverSweepAngle;
+        mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
+        mProgressColor = context.getColor(R.color.udfps_enroll_progress);
+        mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
+
+        mBackgroundPaint = new Paint();
+        mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
+        mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
+        mBackgroundPaint.setAntiAlias(true);
+        mBackgroundPaint.setStyle(Paint.Style.STROKE);
+        mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
+
+        // Background paint color + alpha
+        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+        final TypedArray ta = context.obtainStyledAttributes(attrs);
+        @ColorInt final int tintColor = ta.getColor(0, mBackgroundPaint.getColor());
+        mBackgroundPaint.setColor(tintColor);
+        ta.recycle();
+        TypedValue alpha = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
+        mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
+
+        // Progress should not be color extracted
+        mProgressPaint = new Paint();
+        mProgressPaint.setStrokeWidth(mStrokeWidthPx);
+        mProgressPaint.setColor(mProgressColor);
+        mProgressPaint.setAntiAlias(true);
+        mProgressPaint.setStyle(Paint.Style.STROKE);
+        mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
+
+        mProgressUpdateListener = animation -> {
+            mAnimatedProgress = (float) animation.getAnimatedValue();
+            mInvalidateRunnable.run();
+        };
+
+        mFillColorUpdateListener = animation -> {
+            mProgressPaint.setColor((int) animation.getAnimatedValue());
+            mInvalidateRunnable.run();
+        };
+
+        mOverSweepUpdateListener = animation -> {
+            mOverSweepAngle = (float) animation.getAnimatedValue();
+            mInvalidateRunnable.run();
+        };
+        mOverSweepAnimationRunnable = () -> {
+            if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
+                mOverSweepAnimator.cancel();
+            }
+            mOverSweepAnimator = ValueAnimator.ofFloat(mOverSweepAngle, mMaxOverSweepAngle);
+            mOverSweepAnimator.setDuration(OVER_SWEEP_ANIMATION_DURATION_MS);
+            mOverSweepAnimator.addUpdateListener(mOverSweepUpdateListener);
+            mOverSweepAnimator.start();
+        };
+    }
+
+    /**
+     * Draws this segment to the given canvas.
+     */
+    public void draw(@NonNull Canvas canvas) {
+        final float halfPaddingPx = mStrokeWidthPx / 2f;
+
+        if (mAnimatedProgress < 1f) {
+            // Draw the unfilled background color of the segment.
+            canvas.drawArc(
+                    halfPaddingPx,
+                    halfPaddingPx,
+                    mBounds.right - halfPaddingPx,
+                    mBounds.bottom - halfPaddingPx,
+                    mStartAngle,
+                    mSweepAngle,
+                    false /* useCenter */,
+                    mBackgroundPaint);
+        }
+
+        if (mAnimatedProgress > 0f) {
+            // Draw the filled progress portion of the segment.
+            canvas.drawArc(
+                    halfPaddingPx,
+                    halfPaddingPx,
+                    mBounds.right - halfPaddingPx,
+                    mBounds.bottom - halfPaddingPx,
+                    mStartAngle,
+                    mSweepAngle * mAnimatedProgress + mOverSweepAngle,
+                    false /* useCenter */,
+                    mProgressPaint);
+        }
+    }
+
+    /**
+     * @return The fill progress of this segment, in the range [0, 1]. If fill progress is being
+     * animated, returns the value it is animating to.
+     */
+    public float getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * Updates the fill progress of this segment, animating if necessary.
+     *
+     * @param progress The new fill progress, in the range [0, 1].
+     */
+    public void updateProgress(float progress) {
+        updateProgress(progress, PROGRESS_ANIMATION_DURATION_MS);
+    }
+
+    private void updateProgress(float progress, long animationDurationMs) {
+        if (mProgress == progress) {
+            return;
+        }
+        mProgress = progress;
+
+        if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
+            mProgressAnimator.cancel();
+        }
+
+        mProgressAnimator = ValueAnimator.ofFloat(mAnimatedProgress, progress);
+        mProgressAnimator.setDuration(animationDurationMs);
+        mProgressAnimator.addUpdateListener(mProgressUpdateListener);
+        mProgressAnimator.start();
+    }
+
+    /**
+     * Updates the fill color of this segment, animating if necessary.
+     *
+     * @param isShowingHelp Whether fill color should indicate that a help message is being shown.
+     */
+    public void updateFillColor(boolean isShowingHelp) {
+        if (mIsShowingHelp == isShowingHelp) {
+            return;
+        }
+        mIsShowingHelp = isShowingHelp;
+
+        if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
+            mFillColorAnimator.cancel();
+        }
+
+        @ColorInt final int targetColor = isShowingHelp ? mHelpColor : mProgressColor;
+        mFillColorAnimator = ValueAnimator.ofArgb(mProgressPaint.getColor(), targetColor);
+        mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+        mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
+        mFillColorAnimator.start();
+    }
+
+    /**
+     * Queues and runs the completion animation for this segment.
+     */
+    public void startCompletionAnimation() {
+        final boolean hasCallback = mHandler.hasCallbacks(mOverSweepAnimationRunnable);
+        if (hasCallback || mOverSweepAngle >= mMaxOverSweepAngle) {
+            Log.d(TAG, "startCompletionAnimation skipped: hasCallback = " + hasCallback
+                    + ", mOverSweepAngle = " + mOverSweepAngle);
+            return;
+        }
+
+        // Reset sweep angle back to zero if the animation is being rolled back.
+        if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
+            mOverSweepReverseAnimator.cancel();
+            mOverSweepAngle = 0f;
+        }
+
+        // Clear help color and start filling the segment if it isn't already.
+        if (mAnimatedProgress < 1f) {
+            updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS);
+            updateFillColor(false /* isShowingHelp */);
+        }
+
+        // Queue the animation to run after fill completes.
+        mHandler.postDelayed(mOverSweepAnimationRunnable, OVER_SWEEP_ANIMATION_DELAY_MS);
+    }
+
+    /**
+     * Cancels (and reverses, if necessary) a queued or running completion animation.
+     */
+    public void cancelCompletionAnimation() {
+        // Cancel the animation if it's queued or running.
+        mHandler.removeCallbacks(mOverSweepAnimationRunnable);
+        if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
+            mOverSweepAnimator.cancel();
+        }
+
+        // Roll back the animation if it has at least partially run.
+        if (mOverSweepAngle > 0f) {
+            if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
+                mOverSweepReverseAnimator.cancel();
+            }
+
+            final float completion = mOverSweepAngle / mMaxOverSweepAngle;
+            final long proratedDuration = (long) (OVER_SWEEP_ANIMATION_DURATION_MS * completion);
+            mOverSweepReverseAnimator = ValueAnimator.ofFloat(mOverSweepAngle, 0f);
+            mOverSweepReverseAnimator.setDuration(proratedDuration);
+            mOverSweepReverseAnimator.addUpdateListener(mOverSweepUpdateListener);
+            mOverSweepReverseAnimator.start();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index c83006d..729838e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -73,23 +73,22 @@
     }
 
     void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
+        mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
         mFingerprintDrawable.setEnrollHelper(enrollHelper);
     }
 
     void onEnrollmentProgress(int remaining, int totalSteps) {
         mHandler.post(() -> {
-            mFingerprintProgressDrawable.setEnrollmentProgress(remaining, totalSteps);
+            mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
             mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
         });
     }
 
-    void onLastStepAcquired() {
-        mHandler.post(() -> {
-            mFingerprintProgressDrawable.onLastStepAcquired();
-        });
+    void onEnrollmentHelp(int remaining, int totalSteps) {
+        mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps));
     }
 
-    void onEnrollmentHelp() {
-        mHandler.post(mFingerprintProgressDrawable::onEnrollmentHelp);
+    void onLastStepAcquired() {
+        mHandler.post(mFingerprintProgressDrawable::onLastStepAcquired);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 33fbe7b..af7c352 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -35,21 +35,21 @@
     @NonNull private final UdfpsEnrollHelper mEnrollHelper;
     @NonNull private final UdfpsEnrollHelper.Listener mEnrollHelperListener =
             new UdfpsEnrollHelper.Listener() {
-        @Override
-        public void onEnrollmentProgress(int remaining, int totalSteps) {
-            mView.onEnrollmentProgress(remaining, totalSteps);
-        }
+                @Override
+                public void onEnrollmentProgress(int remaining, int totalSteps) {
+                    mView.onEnrollmentProgress(remaining, totalSteps);
+                }
 
-        @Override
-        public void onLastStepAcquired() {
-            mView.onLastStepAcquired();
-        }
+                @Override
+                public void onEnrollmentHelp(int remaining, int totalSteps) {
+                    mView.onEnrollmentHelp(remaining, totalSteps);
+                }
 
-        @Override
-        public void onEnrollmentHelp() {
-            mView.onEnrollmentHelp();
-        }
-    };
+                @Override
+                public void onLastStepAcquired() {
+                    mView.onLastStepAcquired();
+                }
+            };
 
     protected UdfpsEnrollViewController(
             @NonNull UdfpsEnrollView view,
@@ -81,7 +81,7 @@
     @NonNull
     @Override
     public PointF getTouchTranslation() {
-        if (!mEnrollHelper.isCenterEnrollmentComplete()) {
+        if (!mEnrollHelper.isGuidedEnrollmentStage()) {
             return new PointF(0, 0);
         } else {
             return mEnrollHelper.getNextGuidedEnrollmentPoint();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 23c9408f..bfb63ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -409,21 +409,8 @@
         }
 
         if (mAllowFancy) {
-            // Make brightness appear static position and alpha in through second half.
-            View brightness = mQsPanelController.getBrightnessView();
-            if (brightness != null) {
-                firstPageBuilder.addFloat(brightness, "translationY",
-                        brightness.getMeasuredHeight() * 0.5f, 0);
-                mBrightnessAnimator = new TouchAnimator.Builder()
-                        .addFloat(brightness, "alpha", 0, 1)
-                        .addFloat(brightness, "sliderScaleY", 0.3f, 1)
-                        .setInterpolator(Interpolators.ALPHA_IN)
-                        .setStartDelay(0.3f)
-                        .build();
-                mAllViews.add(brightness);
-            } else {
-                mBrightnessAnimator = null;
-            }
+            animateBrightnessSlider(firstPageBuilder);
+
             mFirstPageAnimator = firstPageBuilder
                     .setListener(this)
                     .build();
@@ -474,20 +461,53 @@
                 .addFloat(tileLayout, "alpha", 0, 1).build();
     }
 
+    private void animateBrightnessSlider(Builder firstPageBuilder) {
+        View qsBrightness = mQsPanelController.getBrightnessView();
+        View qqsBrightness = mQuickQSPanelController.getBrightnessView();
+        if (qqsBrightness != null && qqsBrightness.getVisibility() == View.VISIBLE) {
+            // animating in split shade mode
+            mAnimatedQsViews.add(qsBrightness);
+            mAllViews.add(qqsBrightness);
+            int translationY = getRelativeTranslationY(qsBrightness, qqsBrightness);
+            mBrightnessAnimator = new Builder()
+                    // we need to animate qs brightness even if animation will not be visible,
+                    // as we might start from sliderScaleY set to 0.3 if device was in collapsed QS
+                    // portrait orientation before
+                    .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+                    .addFloat(qqsBrightness, "translationY", 0, translationY)
+                    .build();
+        } else if (qsBrightness != null) {
+            firstPageBuilder.addFloat(qsBrightness, "translationY",
+                    qsBrightness.getMeasuredHeight() * 0.5f, 0);
+            mBrightnessAnimator = new Builder()
+                    .addFloat(qsBrightness, "alpha", 0, 1)
+                    .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1)
+                    .setInterpolator(Interpolators.ALPHA_IN)
+                    .setStartDelay(0.3f)
+                    .build();
+            mAllViews.add(qsBrightness);
+        } else {
+            mBrightnessAnimator = null;
+        }
+    }
+
     private void updateQQSFooterAnimation() {
-        int[] qsPosition = new int[2];
-        int[] qqsPosition = new int[2];
-        View commonView = mQs.getView();
-        getRelativePositionInt(qsPosition, mQSFooterActions, commonView);
-        getRelativePositionInt(qqsPosition, mQQSFooterActions, commonView);
-        int translationY = (qsPosition[1] - qqsPosition[1])
-                - mQuickStatusBarHeader.getOffsetTranslation();
+        int translationY = getRelativeTranslationY(mQSFooterActions, mQQSFooterActions);
         mQQSFooterActionsAnimator = new TouchAnimator.Builder()
                 .addFloat(mQQSFooterActions, "translationY", 0, translationY)
                 .build();
         mAnimatedQsViews.add(mQSFooterActions);
     }
 
+    private int getRelativeTranslationY(View view1, View view2) {
+        int[] qsPosition = new int[2];
+        int[] qqsPosition = new int[2];
+        View commonView = mQs.getView();
+        getRelativePositionInt(qsPosition, view1, commonView);
+        getRelativePositionInt(qqsPosition, view2, commonView);
+        return (qsPosition[1] - qqsPosition[1]) - mQuickStatusBarHeader.getOffsetTranslation();
+    }
+
     private boolean isIconInAnimatedRow(int count) {
         if (mPagedLayout == null) {
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 9025427..70892a7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -258,10 +258,6 @@
         return mView.isLayoutRtl();
     }
 
-    public View getBrightnessView() {
-        return mView.getBrightnessView();
-    }
-
     /** */
     public void setPageListener(PagedTileLayout.PageListener listener) {
         mView.setPageListener(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 42323e3..97568f9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -24,6 +24,7 @@
 import android.content.ComponentName;
 import android.content.res.Configuration;
 import android.metrics.LogMaker;
+import android.view.View;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
@@ -405,6 +406,10 @@
         mUsingHorizontalLayoutChangedListener = listener;
     }
 
+    public View getBrightnessView() {
+        return mView.getBrightnessView();
+    }
+
     /** */
     public static final class TileRecord extends QSPanel.Record {
         public QSTile tile;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
index f09d7b7..7e9f84c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuViewTest.java
@@ -19,8 +19,9 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.View.OVER_SCROLL_ALWAYS;
 import static android.view.View.OVER_SCROLL_NEVER;
+import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
-import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -39,6 +40,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Insets;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.testing.AndroidTestingRunner;
@@ -98,14 +100,15 @@
     private AccessibilityFloatingMenuView mMenuView;
     private RecyclerView mListView = new RecyclerView(mContext);
 
-    private int mScreenHeight;
     private int mMenuWindowHeight;
     private int mMenuHalfWidth;
     private int mMenuHalfHeight;
-    private int mScreenHalfWidth;
-    private int mScreenHalfHeight;
+    private int mDisplayHalfWidth;
+    private int mDisplayHalfHeight;
     private int mMaxWindowX;
     private int mMaxWindowY;
+    private final int mDisplayWindowWidth = 1080;
+    private final int mDisplayWindowHeight = 2340;
 
     @Before
     public void initMenuView() {
@@ -113,7 +116,10 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
-
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, mDisplayWindowWidth,
+                mDisplayWindowHeight));
+        when(mWindowMetrics.getWindowInsets()).thenReturn(fakeDisplayInsets());
         mMenuView = spy(
                 new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition, mListView));
     }
@@ -129,18 +135,16 @@
                 res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_width_height);
         final int menuWidth = padding * 2 + iconWidthHeight;
         final int menuHeight = (padding + iconWidthHeight) * mTargets.size() + padding;
-        final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
-        mScreenHeight = mContext.getResources().getDisplayMetrics().heightPixels;
         mMenuHalfWidth = menuWidth / 2;
         mMenuHalfHeight = menuHeight / 2;
-        mScreenHalfWidth = screenWidth / 2;
-        mScreenHalfHeight = mScreenHeight / 2;
+        mDisplayHalfWidth = mDisplayWindowWidth / 2;
+        mDisplayHalfHeight = mDisplayWindowHeight / 2;
         int marginStartEnd =
                 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT
                         ? margin : 0;
-        mMaxWindowX = screenWidth - marginStartEnd - menuWidth;
+        mMaxWindowX = mDisplayWindowWidth - marginStartEnd - menuWidth;
         mMenuWindowHeight = menuHeight + margin * 2;
-        mMaxWindowY = mScreenHeight - mMenuWindowHeight;
+        mMaxWindowY = mDisplayWindowHeight - mMenuWindowHeight;
     }
 
     @Test
@@ -279,15 +283,15 @@
         final MotionEvent moveEvent =
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
-                        /* screenCenterX */mScreenHalfWidth
-                                - /* offsetXToScreenLeftHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */mDisplayHalfWidth
+                                - /* offsetXToDisplayLeftHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
-                        /* screenCenterX */ mScreenHalfWidth
-                                - /* offsetXToScreenLeftHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */ mDisplayHalfWidth
+                                - /* offsetXToDisplayLeftHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
 
         listView.dispatchTouchEvent(downEvent);
         listView.dispatchTouchEvent(moveEvent);
@@ -315,15 +319,15 @@
         final MotionEvent moveEvent =
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
-                        /* screenCenterX */mScreenHalfWidth
-                                + /* offsetXToScreenRightHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */mDisplayHalfWidth
+                                + /* offsetXToDisplayRightHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
-                        /* screenCenterX */ mScreenHalfWidth
-                                + /* offsetXToScreenRightHalfRegion */ 10,
-                        /* screenCenterY */ mScreenHalfHeight);
+                        /* displayCenterX */ mDisplayHalfWidth
+                                + /* offsetXToDisplayRightHalfRegion */ 10,
+                        /* displayCenterY */ mDisplayHalfHeight);
 
         listView.dispatchTouchEvent(downEvent);
         listView.dispatchTouchEvent(moveEvent);
@@ -332,12 +336,12 @@
 
         assertThat((float) menuView.mCurrentLayoutParams.x).isWithin(1.0f).of(mMaxWindowX);
         assertThat((float) menuView.mCurrentLayoutParams.y).isWithin(1.0f).of(
-                /* newWindowY = screenCenterY - offsetY */ mScreenHalfHeight - mMenuHalfHeight);
+                /* newWindowY = displayCenterY - offsetY */ mDisplayHalfHeight - mMenuHalfHeight);
     }
 
 
     @Test
-    public void tapOnAndDragMenuToScreenSide_transformShapeHalfOval() {
+    public void tapOnAndDragMenuToDisplaySide_transformShapeHalfOval() {
         final Position alignRightPosition = new Position(1.0f, 0.8f);
         final RecyclerView listView = new RecyclerView(mContext);
         final AccessibilityFloatingMenuView menuView = new AccessibilityFloatingMenuView(mContext,
@@ -355,13 +359,13 @@
                 mMotionEventHelper.obtainMotionEvent(2, 3,
                         MotionEvent.ACTION_MOVE,
                         /* downX */(currentWindowX + mMenuHalfWidth)
-                                + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+                                + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
                         /* downY */ (currentWindowY +  mMenuHalfHeight));
         final MotionEvent upEvent =
                 mMotionEventHelper.obtainMotionEvent(4, 5,
                         MotionEvent.ACTION_UP,
                         /* downX */(currentWindowX + mMenuHalfWidth)
-                                + /* offsetXToScreenRightSide */ mMenuHalfWidth,
+                                + /* offsetXToDisplayRightSide */ mMenuHalfWidth,
                         /* downY */ (currentWindowY +  mMenuHalfHeight));
 
         listView.dispatchTouchEvent(downEvent);
@@ -423,7 +427,7 @@
     }
 
     @Test
-    public void showMenuAndIme_withHigherIme_alignScreenTopEdge() {
+    public void showMenuAndIme_withHigherIme_alignDisplayTopEdge() {
         final int offset = 99999;
 
         setupBasicMenuView(mMenuView);
@@ -475,10 +479,21 @@
     private WindowInsets fakeImeInsetWith(AccessibilityFloatingMenuView menuView, int offset) {
         // Ensure the keyboard has overlapped on the menu view.
         final int fakeImeHeight =
-                mScreenHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight) + offset;
+                mDisplayWindowHeight - (menuView.mCurrentLayoutParams.y + mMenuWindowHeight)
+                        + offset;
         return new WindowInsets.Builder()
-                .setVisible(ime() | navigationBars(), true)
-                .setInsets(ime() | navigationBars(), Insets.of(0, 0, 0, fakeImeHeight))
+                .setVisible(ime(), true)
+                .setInsets(ime(), Insets.of(0, 0, 0, fakeImeHeight))
+                .build();
+    }
+
+    private WindowInsets fakeDisplayInsets() {
+        final int fakeStatusBarHeight = 75;
+        final int fakeNavigationBarHeight = 125;
+        return new WindowInsets.Builder()
+                .setVisible(systemBars() | displayCutout(), true)
+                .setInsets(systemBars() | displayCutout(),
+                        Insets.of(0, fakeStatusBarHeight, 0, fakeNavigationBarHeight))
                 .build();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
index eb1f15b..3553a0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/BaseTooltipViewTest.java
@@ -23,12 +23,16 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.test.filters.SmallTest;
@@ -52,6 +56,9 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private AccessibilityFloatingMenuView mMenuView;
     private BaseTooltipView mToolTipView;
 
@@ -66,6 +73,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mMenuView = new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition);
         mToolTipView = new BaseTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
index ca4e3e9..9eba49d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DockTooltipViewTest.java
@@ -21,12 +21,16 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.filters.SmallTest;
 
@@ -49,6 +53,9 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private AccessibilityFloatingMenuView mMenuView;
     private DockTooltipView mDockTooltipView;
     private final Position mPlaceholderPosition = new Position(0.0f, 0.0f);
@@ -62,6 +69,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mMenuView = spy(new AccessibilityFloatingMenuView(mContext, mPlaceholderPosition));
         mDockTooltipView = new DockTooltipView(mContext, mMenuView);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
index dae4364..ea104a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/ItemDelegateCompatTest.java
@@ -22,12 +22,15 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -57,6 +60,8 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
     private RecyclerView mListView;
     private AccessibilityFloatingMenuView mMenuView;
     private ItemDelegateCompat mItemDelegateCompat;
@@ -69,6 +74,9 @@
         doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
                 mWindowManager).getMaximumWindowMetrics();
         mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect());
+        when(mWindowMetrics.getWindowInsets()).thenReturn(new WindowInsets.Builder().build());
 
         mListView = new RecyclerView(mContext);
         mMenuView =
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 20ad9e1..e00a63e 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -98,8 +98,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.permission.PermissionControllerManager;
-import android.provider.Settings;
-import android.provider.SettingsStringUtil.ComponentNameSet;
 import android.text.BidiFormatter;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -632,6 +630,12 @@
             registerDevicePresenceListenerActive(packageName, deviceAddress, false);
         }
 
+        @Override
+        public void receiveMessage(int messageId, int associationId, byte[] message)
+                throws RemoteException {
+            //TODO: b/199427116
+        }
+
         private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
                 boolean active) throws RemoteException {
             getContext().enforceCallingOrSelfPermission(
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
index 9f3045e..3a90a95 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java
@@ -87,7 +87,7 @@
         mId = sessionId;
         mUid = uid;
         mContentCaptureContext = new ContentCaptureContext(/* clientContext= */ null,
-                activityId, appComponentName, displayId, flags);
+                activityId, appComponentName, displayId, activityToken, flags);
         mSessionStateReceiver = sessionStateReceiver;
         try {
             sessionStateReceiver.asBinder().linkToDeath(() -> onClientDeath(), 0);
diff --git a/services/core/java/com/android/server/am/CacheOomRanker.java b/services/core/java/com/android/server/am/CacheOomRanker.java
index 7413808..e6ffcfc 100644
--- a/services/core/java/com/android/server/am/CacheOomRanker.java
+++ b/services/core/java/com/android/server/am/CacheOomRanker.java
@@ -364,6 +364,7 @@
                 // First element is total RSS:
                 // frameworks/base/core/jni/android_util_Process.cpp:1192
                 scoredProcessRecord.proc.mState.setCacheOomRankerRss(rss[0], nowMs);
+                scoredProcessRecord.proc.mProfile.setLastRss(rss[0]);
             }
         }
 
diff --git a/services/core/java/com/android/server/am/ComponentAliasResolver.java b/services/core/java/com/android/server/am/ComponentAliasResolver.java
index 00b0e83..23553a7 100644
--- a/services/core/java/com/android/server/am/ComponentAliasResolver.java
+++ b/services/core/java/com/android/server/am/ComponentAliasResolver.java
@@ -157,6 +157,9 @@
      */
     public void update(String overrides) {
         synchronized (mLock) {
+            if (mPlatformCompat == null) {
+                return; // System not ready.
+            }
             final boolean enabled = mPlatformCompat.isChangeEnabledByPackageName(
                     USE_EXPERIMENTAL_COMPONENT_ALIAS, "android", UserHandle.USER_SYSTEM);
             if (enabled != mEnabled) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1c4b9ce..6673b1b5 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -244,7 +244,7 @@
      */
     private static final int FLAG_ADJUST_VOLUME = 1;
 
-    private final Context mContext;
+    final Context mContext;
     private final ContentResolver mContentResolver;
     private final AppOpsManager mAppOps;
 
@@ -319,6 +319,7 @@
     private static final int MSG_A2DP_DEV_CONFIG_CHANGE = 39;
     private static final int MSG_DISPATCH_AUDIO_MODE = 40;
     private static final int MSG_ROUTING_UPDATED = 41;
+    private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
 
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -7585,6 +7586,10 @@
                     mAudioEventWakeLock.release();
                     break;
 
+                case MSG_INIT_HEADTRACKING_SENSORS:
+                    mSpatializerHelper.onInitSensors(/*init*/ msg.arg1 == 1);
+                    break;
+
                 case MSG_CHECK_MUSIC_ACTIVE:
                     onCheckMusicActive((String) msg.obj);
                     break;
@@ -8457,6 +8462,18 @@
         mSpatializerHelper.getEffectParameter(key, value);
     }
 
+    /**
+     * post a message to schedule init/release of head tracking sensors
+     * @param init initialization if true, release if false
+     */
+    void postInitSpatializerHeadTrackingSensors(boolean init) {
+        sendMsg(mAudioHandler,
+                MSG_INIT_HEADTRACKING_SENSORS,
+                SENDMSG_REPLACE,
+                /*arg1*/ init ? 1 : 0,
+                0, TAG, /*delay*/ 0);
+    }
+
     //==========================================================================================
     private boolean readCameraSoundForced() {
         return SystemProperties.getBoolean("audio.camerasound.force", false) ||
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index ccaa96d..66afe9b 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -18,6 +18,9 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioFormat;
@@ -28,6 +31,7 @@
 import android.media.ISpatializerHeadToSoundStagePoseCallback;
 import android.media.ISpatializerHeadTrackingCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.SpatializationLevel;
 import android.media.Spatializer;
 import android.media.SpatializerHeadTrackingMode;
 import android.os.RemoteCallbackList;
@@ -45,6 +49,7 @@
 
     private static final String TAG = "AS.SpatializerHelper";
     private static final boolean DEBUG = true;
+    private static final boolean DEBUG_MORE = false;
 
     private static void logd(String s) {
         if (DEBUG) {
@@ -54,6 +59,7 @@
 
     private final @NonNull AudioSystemAdapter mASA;
     private final @NonNull AudioService mAudioService;
+    private @Nullable SensorManager mSensorManager;
 
     //------------------------------------------------------------
     // Spatializer state machine
@@ -127,7 +133,7 @@
             for (byte level : levels) {
                 logd("found support for level: " + level);
                 if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL) {
-                    logd("Setting Spatializer to LEVEL_MULTICHANNEL");
+                    logd("Setting capable level to LEVEL_MULTICHANNEL");
                     mCapableSpatLevel = level;
                     break;
                 }
@@ -191,9 +197,16 @@
         public void onLevelChanged(byte level) {
             logd("SpatializerCallback.onLevelChanged level:" + level);
             synchronized (SpatializerHelper.this) {
-                mSpatLevel = level;
+                mSpatLevel = spatializationLevelToSpatializerInt(level);
             }
             // TODO use reported spat level to change state
+
+            // init sensors
+            if (level == SpatializationLevel.NONE) {
+                initSensors(/*init*/false);
+            } else {
+                postInitSensors(true);
+            }
         }
     };
 
@@ -224,7 +237,7 @@
                         + " invalid transform length" + headToStage.length);
                 return;
             }
-            if (DEBUG) {
+            if (DEBUG_MORE) {
                 // 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters
                 StringBuilder t = new StringBuilder(42);
                 for (float val : headToStage) {
@@ -641,36 +654,6 @@
         }
     }
 
-    private int headTrackingModeTypeToSpatializerInt(byte mode) {
-        switch (mode) {
-            case SpatializerHeadTrackingMode.OTHER:
-                return Spatializer.HEAD_TRACKING_MODE_OTHER;
-            case SpatializerHeadTrackingMode.DISABLED:
-                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
-            case SpatializerHeadTrackingMode.RELATIVE_WORLD:
-                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
-            case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
-                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
-            default:
-                throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
-        }
-    }
-
-    private byte spatializerIntToHeadTrackingModeType(int sdkMode) {
-        switch (sdkMode) {
-            case Spatializer.HEAD_TRACKING_MODE_OTHER:
-                return SpatializerHeadTrackingMode.OTHER;
-            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
-                return SpatializerHeadTrackingMode.DISABLED;
-            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
-                return SpatializerHeadTrackingMode.RELATIVE_WORLD;
-            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
-                return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
-            default:
-                throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
-        }
-    }
-
     private boolean checkSpatForHeadTracking(String funcName) {
         switch (mState) {
             case STATE_UNINITIALIZED:
@@ -792,4 +775,101 @@
             Log.e(TAG, "Error in getParameter for key:" + key, e);
         }
     }
+
+    //------------------------------------------------------
+    // sensors
+    private void initSensors(boolean init) {
+        if (mSensorManager == null) {
+            mSensorManager = (SensorManager)
+                    mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
+        }
+        final int headHandle;
+        final int screenHandle;
+        if (init) {
+            if (mSensorManager == null) {
+                Log.e(TAG, "Null SensorManager, can't init sensors");
+                return;
+            }
+            // TODO replace with dynamic association of sensor for headtracker
+            Sensor headSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
+            headHandle = headSensor.getHandle();
+            //Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+            //screenHandle = deviceSensor.getHandle();
+            screenHandle = -1;
+        } else {
+            // -1 is disable value
+            screenHandle = -1;
+            headHandle = -1;
+        }
+        try {
+            Log.i(TAG, "setScreenSensor:" + screenHandle);
+            mSpat.setScreenSensor(screenHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
+        }
+        try {
+            Log.i(TAG, "setHeadSensor:" + headHandle);
+            mSpat.setHeadSensor(headHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
+        }
+    }
+
+    private void postInitSensors(boolean init) {
+        mAudioService.postInitSpatializerHeadTrackingSensors(init);
+    }
+
+    synchronized void onInitSensors(boolean init) {
+        final int[] modes = getSupportedHeadTrackingModes();
+        if (modes.length == 0) {
+            Log.i(TAG, "not initializing sensors, no headtracking supported");
+            return;
+        }
+        initSensors(init);
+    }
+
+    //------------------------------------------------------
+    // SDK <-> AIDL converters
+    private static int headTrackingModeTypeToSpatializerInt(byte mode) {
+        switch (mode) {
+            case SpatializerHeadTrackingMode.OTHER:
+                return Spatializer.HEAD_TRACKING_MODE_OTHER;
+            case SpatializerHeadTrackingMode.DISABLED:
+                return Spatializer.HEAD_TRACKING_MODE_DISABLED;
+            case SpatializerHeadTrackingMode.RELATIVE_WORLD:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
+            case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
+                return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
+        }
+    }
+
+    private static byte spatializerIntToHeadTrackingModeType(int sdkMode) {
+        switch (sdkMode) {
+            case Spatializer.HEAD_TRACKING_MODE_OTHER:
+                return SpatializerHeadTrackingMode.OTHER;
+            case Spatializer.HEAD_TRACKING_MODE_DISABLED:
+                return SpatializerHeadTrackingMode.DISABLED;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
+                return SpatializerHeadTrackingMode.RELATIVE_WORLD;
+            case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
+                return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
+            default:
+                throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
+        }
+    }
+
+    private static int spatializationLevelToSpatializerInt(byte level) {
+        switch (level) {
+            case SpatializationLevel.NONE:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+            case SpatializationLevel.SPATIALIZER_MULTICHANNEL:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL;
+            case SpatializationLevel.SPATIALIZER_MCHAN_BED_PLUS_OBJECTS:
+                return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS;
+            default:
+                throw(new IllegalArgumentException("Unexpected spatializer level:" + level));
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 86d7dcd..9be9505 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -16,8 +16,6 @@
 
 package com.android.server.input;
 
-import static android.view.Surface.ROTATION_0;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
@@ -40,7 +38,6 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.database.ContentObserver;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayViewport;
@@ -100,7 +97,6 @@
 import android.view.InputEvent;
 import android.view.InputMonitor;
 import android.view.KeyEvent;
-import android.view.MotionEvent;
 import android.view.PointerIcon;
 import android.view.Surface;
 import android.view.VerifiedInputEvent;
@@ -595,20 +591,9 @@
 
     private void setDisplayViewportsInternal(List<DisplayViewport> viewports) {
         final DisplayViewport[] vArray = new DisplayViewport[viewports.size()];
-        if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
-            // Remove display projection information from DisplayViewport, leaving only the
-            // orientation. The display projection will be built-into the window transforms.
-            for (int i = viewports.size() - 1; i >= 0; --i) {
-                final DisplayViewport v = vArray[i] = viewports.get(i).makeCopy();
-                // Note: the deviceWidth/Height are in rotated with the orientation.
-                v.logicalFrame.set(0, 0, v.deviceWidth, v.deviceHeight);
-                v.physicalFrame.set(0, 0, v.deviceWidth, v.deviceHeight);
-            }
-        } else {
             for (int i = viewports.size() - 1; i >= 0; --i) {
                 vArray[i] = viewports.get(i);
             }
-        }
         nativeSetDisplayViewports(mPtr, vArray);
     }
 
@@ -828,38 +813,6 @@
                 && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
             throw new IllegalArgumentException("mode is invalid");
         }
-        if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
-            // Motion events that are pointer events or relative mouse events will need to have the
-            // inverse display rotation applied to them.
-            if (event instanceof MotionEvent
-                    && (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
-                    || event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE))) {
-                Context displayContext = getContextForDisplay(event.getDisplayId());
-                if (displayContext == null) {
-                    displayContext = Objects.requireNonNull(
-                            getContextForDisplay(Display.DEFAULT_DISPLAY));
-                }
-                final Display display = displayContext.getDisplay();
-                final int rotation = display.getRotation();
-                if (rotation != ROTATION_0) {
-                    final MotionEvent motion = (MotionEvent) event;
-                    // Injections are currently expected to be in the space of the injector (ie.
-                    // usually assumed to be post-rotated). Thus we need to un-rotate into raw
-                    // input coordinates for dispatch.
-                    final Point sz = new Point();
-                    if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
-                        display.getRealSize(sz);
-                        if ((rotation % 2) != 0) {
-                            final int tmpX = sz.x;
-                            sz.x = sz.y;
-                            sz.y = tmpX;
-                        }
-                    }
-                    motion.applyTransform(MotionEvent.createRotateMatrix(
-                            (4 - rotation), sz.x, sz.y));
-                }
-            }
-        }
 
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index a25392a..686926f 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -293,7 +293,7 @@
                     }, UserHandle.USER_ALL);
         }
 
-        if (mContextHubWrapper.supportsMicrophoneDisableSettingNotifications()) {
+        if (mContextHubWrapper.supportsMicrophoneSettingNotifications()) {
             sendMicrophoneDisableSettingUpdateForCurrentUser();
 
             mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
@@ -1100,7 +1100,10 @@
      */
     private void sendMicrophoneDisableSettingUpdate(boolean enabled) {
         Log.d(TAG, "Mic Disabled Setting: " + enabled);
-        mContextHubWrapper.onMicrophoneDisableSettingChanged(enabled);
+        // The SensorPrivacyManager reports if microphone privacy was enabled,
+        // which translates to microphone access being disabled (and vice-versa).
+        // With this in mind, we flip the argument before piping it to CHRE.
+        mContextHubWrapper.onMicrophoneSettingChanged(!enabled);
     }
 
     /**
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index 13bcc9b..74630d1 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -228,15 +228,15 @@
     public abstract void onAirplaneModeSettingChanged(boolean enabled);
 
     /**
-     * @return True if this version of the Contexthub HAL supports microphone disable setting
+     * @return True if this version of the Contexthub HAL supports microphone setting
      * notifications.
      */
-    public abstract boolean supportsMicrophoneDisableSettingNotifications();
+    public abstract boolean supportsMicrophoneSettingNotifications();
 
     /**
-     * Notifies the Contexthub implementation of a microphone disable setting change.
+     * Notifies the Contexthub implementation of a microphone setting change.
      */
-    public abstract void onMicrophoneDisableSettingChanged(boolean enabled);
+    public abstract void onMicrophoneSettingChanged(boolean enabled);
 
     /**
      * Sends a message to the Context Hub.
@@ -380,7 +380,7 @@
             return true;
         }
 
-        public boolean supportsMicrophoneDisableSettingNotifications() {
+        public boolean supportsMicrophoneSettingNotifications() {
             return true;
         }
 
@@ -395,7 +395,7 @@
             onSettingChanged(android.hardware.contexthub.Setting.AIRPLANE_MODE, enabled);
         }
 
-        public void onMicrophoneDisableSettingChanged(boolean enabled) {
+        public void onMicrophoneSettingChanged(boolean enabled) {
             onSettingChanged(android.hardware.contexthub.Setting.MICROPHONE, enabled);
         }
 
@@ -615,7 +615,7 @@
             return false;
         }
 
-        public boolean supportsMicrophoneDisableSettingNotifications() {
+        public boolean supportsMicrophoneSettingNotifications() {
             return false;
         }
 
@@ -628,7 +628,7 @@
         public void onAirplaneModeSettingChanged(boolean enabled) {
         }
 
-        public void onMicrophoneDisableSettingChanged(boolean enabled) {
+        public void onMicrophoneSettingChanged(boolean enabled) {
         }
     }
 
@@ -660,7 +660,7 @@
             return false;
         }
 
-        public boolean supportsMicrophoneDisableSettingNotifications() {
+        public boolean supportsMicrophoneSettingNotifications() {
             return false;
         }
 
@@ -679,7 +679,7 @@
         public void onAirplaneModeSettingChanged(boolean enabled) {
         }
 
-        public void onMicrophoneDisableSettingChanged(boolean enabled) {
+        public void onMicrophoneSettingChanged(boolean enabled) {
         }
     }
 
@@ -721,7 +721,7 @@
             return true;
         }
 
-        public boolean supportsMicrophoneDisableSettingNotifications() {
+        public boolean supportsMicrophoneSettingNotifications() {
             return true;
         }
 
@@ -740,12 +740,9 @@
                     enabled ? SettingValue.ENABLED : SettingValue.DISABLED);
         }
 
-        public void onMicrophoneDisableSettingChanged(boolean enabled) {
-            // The SensorPrivacyManager reports if microphone privacy was enabled,
-            // which translates to microphone access being disabled (and vice-versa).
-            // With this in mind, we flip the argument before piping it to CHRE.
+        public void onMicrophoneSettingChanged(boolean enabled) {
             sendSettingChanged(android.hardware.contexthub.V1_2.Setting.MICROPHONE,
-                    enabled ? SettingValue.DISABLED : SettingValue.ENABLED);
+                    enabled ? SettingValue.ENABLED : SettingValue.DISABLED);
         }
 
         public void registerCallback(int contextHubId, ICallback callback) throws RemoteException {
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 4ae058d..ddac9cd 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -45,9 +45,11 @@
     enum Status {
         RUNNING,
         FINISHED,
+        FINISHED_UNEXPECTED,  // Didn't terminate in the usual way.
         FORWARDED_TO_INPUT_DEVICES,
         CANCELLED,
         IGNORED_ERROR_APP_OPS,
+        IGNORED_ERROR_TOKEN,
         IGNORED,
         IGNORED_APP_OPS,
         IGNORED_BACKGROUND,
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 630bf0a..25321c1 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -47,6 +47,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
 import java.util.Queue;
 
@@ -110,6 +111,8 @@
 
     private volatile boolean mStop;
     private volatile boolean mForceStop;
+    // Variable only set and read in main thread.
+    private boolean mCalledVibrationCompleteCallback = false;
 
     VibrationThread(Vibration vib, VibrationSettings vibrationSettings,
             DeviceVibrationEffectAdapter effectAdapter,
@@ -150,18 +153,53 @@
 
     @Override
     public void run() {
-        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+        // Structured to guarantee the vibrators completed and released callbacks at the end of
+        // thread execution. Both of these callbacks are exclusively called from this thread.
+        try {
+            try {
+                Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
+                runWithWakeLock();
+            } finally {
+                clientVibrationCompleteIfNotAlready(Vibration.Status.FINISHED_UNEXPECTED);
+            }
+        } finally {
+            mCallbacks.onVibratorsReleased();
+        }
+    }
+
+    /** Runs the VibrationThread ensuring that the wake lock is acquired and released. */
+    private void runWithWakeLock() {
         mWakeLock.setWorkSource(mWorkSource);
         mWakeLock.acquire();
         try {
+            runWithWakeLockAndDeathLink();
+        } finally {
+            mWakeLock.release();
+        }
+    }
+
+    /**
+     * Runs the VibrationThread with the binder death link, handling link/unlink failures.
+     * Called from within runWithWakeLock.
+     */
+    private void runWithWakeLockAndDeathLink() {
+        try {
             mVibration.token.linkToDeath(this, 0);
-            playVibration();
-            mCallbacks.onVibratorsReleased();
         } catch (RemoteException e) {
             Slog.e(TAG, "Error linking vibration to token death", e);
+            clientVibrationCompleteIfNotAlready(Vibration.Status.IGNORED_ERROR_TOKEN);
+            return;
+        }
+        // Ensure that the unlink always occurs now.
+        try {
+            // This is the actual execution of the vibration.
+            playVibration();
         } finally {
-            mVibration.token.unlinkToDeath(this, 0);
-            mWakeLock.release();
+            try {
+                mVibration.token.unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                Slog.wtf(TAG, "Failed to unlink token", e);
+            }
         }
     }
 
@@ -219,6 +257,16 @@
         }
     }
 
+    // Indicate that the vibration is complete. This can be called multiple times only for
+    // convenience of handling error conditions - an error after the client is complete won't
+    // affect the status.
+    private void clientVibrationCompleteIfNotAlready(Vibration.Status completedStatus) {
+        if (!mCalledVibrationCompleteCallback) {
+            mCalledVibrationCompleteCallback = true;
+            mCallbacks.onVibrationCompleted(mVibration.id, completedStatus);
+        }
+    }
+
     private void playVibration() {
         Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
         try {
@@ -226,7 +274,6 @@
             final int sequentialEffectSize = sequentialEffect.getEffects().size();
             mStepQueue.offer(new StartVibrateStep(sequentialEffect));
 
-            Vibration.Status status = null;
             while (!mStepQueue.isEmpty()) {
                 long waitTime;
                 synchronized (mLock) {
@@ -242,13 +289,12 @@
                 if (waitTime <= 0) {
                     mStepQueue.consumeNext();
                 }
-                Vibration.Status currentStatus = mStop ? Vibration.Status.CANCELLED
+                Vibration.Status status = mStop ? Vibration.Status.CANCELLED
                         : mStepQueue.calculateVibrationStatus(sequentialEffectSize);
-                if (status == null && currentStatus != Vibration.Status.RUNNING) {
+                if (status != Vibration.Status.RUNNING && !mCalledVibrationCompleteCallback) {
                     // First time vibration stopped running, start clean-up tasks and notify
                     // callback immediately.
-                    status = currentStatus;
-                    mCallbacks.onVibrationCompleted(mVibration.id, status);
+                    clientVibrationCompleteIfNotAlready(status);
                     if (status == Vibration.Status.CANCELLED) {
                         mStepQueue.cancel();
                     }
@@ -256,19 +302,10 @@
                 if (mForceStop) {
                     // Cancel every step and stop playing them right away, even clean-up steps.
                     mStepQueue.cancelImmediately();
+                    clientVibrationCompleteIfNotAlready(Vibration.Status.CANCELLED);
                     break;
                 }
             }
-
-            if (status == null) {
-                status = mStepQueue.calculateVibrationStatus(sequentialEffectSize);
-                if (status == Vibration.Status.RUNNING) {
-                    Slog.w(TAG, "Something went wrong, step queue completed but vibration status"
-                            + " is still RUNNING for vibration " + mVibration.id);
-                    status = Vibration.Status.FINISHED;
-                }
-                mCallbacks.onVibrationCompleted(mVibration.id, status);
-            }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 71ac730..bab5a61 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.Manifest.permission.ACTIVITY_EMBEDDING;
 import static android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND;
 import static android.app.Activity.RESULT_CANCELED;
 import static android.app.ActivityManager.START_ABORTED;
@@ -1953,38 +1952,43 @@
             }
         }
 
-        if (mInTaskFragment != null && mInTaskFragment.getTask() != null) {
-            final int hostUid = mInTaskFragment.getTask().effectiveUid;
-            final int embeddingUid = targetTask != null ? targetTask.effectiveUid : r.getUid();
-            if (!canTaskBeEmbedded(hostUid, embeddingUid)) {
-                Slog.e(TAG, "Cannot embed activity to a task owned by " + hostUid + " targetTask= "
-                        + targetTask);
-                return START_PERMISSION_DENIED;
-            }
+        if (mInTaskFragment != null && !canEmbedActivity(mInTaskFragment, r, newTask, targetTask)) {
+            Slog.e(TAG, "Permission denied: Cannot embed " + r + " to " + mInTaskFragment.getTask()
+                    + " targetTask= " + targetTask);
+            return START_PERMISSION_DENIED;
         }
 
         return START_SUCCESS;
     }
 
     /**
-     * Return {@code true} if the {@param task} can embed another task.
-     * @param hostUid the uid of the host task
-     * @param embeddedUid the uid of the task the are going to be embedded
+     * Return {@code true} if an activity can be embedded to the TaskFragment.
+     * @param taskFragment the TaskFragment for embedding.
+     * @param starting the starting activity.
+     * @param newTask whether the starting activity is going to be launched on a new task.
+     * @param targetTask the target task for launching activity, which could be different from
+     *                   the one who hosting the embedding.
      */
-    private boolean canTaskBeEmbedded(int hostUid, int embeddedUid) {
+    private boolean canEmbedActivity(@NonNull TaskFragment taskFragment, ActivityRecord starting,
+            boolean newTask, Task targetTask) {
+        final Task hostTask = taskFragment.getTask();
+        if (hostTask == null) {
+            return false;
+        }
+
         // Allowing the embedding if the task is owned by system.
+        final int hostUid = hostTask.effectiveUid;
         if (hostUid == Process.SYSTEM_UID) {
             return true;
         }
 
-        // Allowing embedding if the host task is owned by an app that has the ACTIVITY_EMBEDDING
-        // permission
-        if (mService.checkPermission(ACTIVITY_EMBEDDING, -1, hostUid) == PERMISSION_GRANTED) {
-            return true;
+        // Not allowed embedding an activity of another app.
+        if (hostUid != starting.getUid()) {
+            return false;
         }
 
-        // Allowing embedding if it is from the same app that owned the task
-        return hostUid == embeddedUid;
+        // Not allowed embedding task.
+        return !newTask && (targetTask == null || targetTask == hostTask);
     }
 
     /**
@@ -2801,10 +2805,15 @@
                 newParent = mInTaskFragment;
             }
         } else {
-            // Use the child TaskFragment (if any) as the new parent if the activity can be embedded
             final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
                     false /* includingEmbeddedTask */);
-            newParent = top != null ? top.getTaskFragment() : task;
+            final TaskFragment taskFragment = top != null ? top.getTaskFragment() : null;
+            if (taskFragment != null && taskFragment.isEmbedded()
+                    && task.effectiveUid == mStartActivity.getUid()) {
+                // Use the embedded TaskFragment of the top activity as the new parent if the
+                // activity can be embedded.
+                newParent = top.getTaskFragment();
+            }
         }
 
         if (mStartActivity.getTaskFragment() == null
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 2805dce..795286d 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -170,6 +170,9 @@
      */
     @VisibleForTesting
     void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
+        if (shouldDisableSnapshots()) {
+            return;
+        }
         mSkipClosingAppSnapshotTasks.addAll(tasks);
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
index 1bdc235..cf10c53 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -28,6 +28,7 @@
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.traces.common.FlickerComponentName
 import com.google.common.truth.Truth
 import org.junit.FixMethodOrder
@@ -107,7 +108,7 @@
      * Checks that the app layer doesn't exist at the start of the transition, that it is
      * created (invisible) and becomes visible during the transition
      */
-    @Presubmit
+    @FlakyTest
     @Test
     fun appLayerBecomesVisible() {
         testSpec.assertLayers {
@@ -168,6 +169,11 @@
     /** {@inheritDoc} */
     @FlakyTest
     @Test
+    override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible()
+
+    /** {@inheritDoc} */
+    @FlakyTest
+    @Test
     override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
             super.visibleWindowsShownMoreThanOneConsecutiveEntry()