Merge changes I9be69af5,Ib85ee393 into rvc-dev

* changes:
  Clear uninstall block when removing DO.
  Clean up app restrictions when removing DO or PO
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java
index d1e9e4e..50f4ddd 100644
--- a/apex/media/framework/java/android/media/MediaParser.java
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -1412,14 +1412,12 @@
         setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height);
 
         List<byte[]> initData = format.initializationData;
-        if (initData != null) {
-            for (int i = 0; i < initData.size(); i++) {
-                result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i)));
-            }
+        for (int i = 0; i < initData.size(); i++) {
+            result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i)));
         }
+        setPcmEncoding(format, result);
         setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language);
         setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize);
-        setOptionalMediaFormatInt(result, MediaFormat.KEY_PCM_ENCODING, format.pcmEncoding);
         setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees);
         setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate);
         setOptionalMediaFormatInt(
@@ -1462,6 +1460,27 @@
         return result;
     }
 
+    private static void setPcmEncoding(Format format, MediaFormat result) {
+        int exoPcmEncoding = format.pcmEncoding;
+        setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding);
+        int mediaFormatPcmEncoding;
+        switch (exoPcmEncoding) {
+            case C.ENCODING_PCM_8BIT:
+                mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT;
+                break;
+            case C.ENCODING_PCM_16BIT:
+                mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT;
+                break;
+            case C.ENCODING_PCM_FLOAT:
+                mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT;
+                break;
+            default:
+                // No matching value. Do nothing.
+                return;
+        }
+        result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding);
+    }
+
     private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) {
         if (value != Format.NO_VALUE) {
             mediaFormat.setInteger(key, value);
diff --git a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java b/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java
index 103b53e..c268ff4 100644
--- a/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java
+++ b/apex/sdkextensions/framework/java/android/os/ext/SdkExtensions.java
@@ -62,7 +62,11 @@
         if (sdk < VERSION_CODES.R) {
             throw new IllegalArgumentException(String.valueOf(sdk) + " does not have extensions");
         }
-        return R_EXTENSION_INT;
+
+        if (sdk == VERSION_CODES.R) {
+            return R_EXTENSION_INT;
+        }
+        return 0;
     }
 
 }
diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp
index 4410f1c..bb32dd2 100644
--- a/cmds/screencap/screencap.cpp
+++ b/cmds/screencap/screencap.cpp
@@ -105,7 +105,7 @@
     char *cmd[] = {
         (char*) "am",
         (char*) "broadcast",
-        (char*) "am",
+        (char*) "-a",
         (char*) "android.intent.action.MEDIA_SCANNER_SCAN_FILE",
         (char*) "-d",
         &filePath[0],
diff --git a/cmds/statsd/src/main.cpp b/cmds/statsd/src/main.cpp
index e394533..cd9c4e5 100644
--- a/cmds/statsd/src/main.cpp
+++ b/cmds/statsd/src/main.cpp
@@ -37,6 +37,7 @@
 using std::make_shared;
 
 shared_ptr<StatsService> gStatsService = nullptr;
+sp<StatsSocketListener> gSocketListener = nullptr;
 
 void signalHandler(int sig) {
     if (sig == SIGPIPE) {
@@ -47,6 +48,7 @@
         return;
     }
 
+    if (gSocketListener != nullptr) gSocketListener->stopListener();
     if (gStatsService != nullptr) gStatsService->Terminate();
     ALOGW("statsd terminated on receiving signal %d.", sig);
     exit(1);
@@ -92,11 +94,11 @@
 
     gStatsService->Startup();
 
-    sp<StatsSocketListener> socketListener = new StatsSocketListener(eventQueue);
+    gSocketListener = new StatsSocketListener(eventQueue);
 
     ALOGI("Statsd starts to listen to socket.");
     // Backlog and /proc/sys/net/unix/max_dgram_qlen set to large value
-    if (socketListener->startListener(600)) {
+    if (gSocketListener->startListener(600)) {
         exit(1);
     }
 
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 67334f5..18e5c3d 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -494,6 +494,13 @@
      */
     public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
 
+    /**
+     * Action to send the KEYCODE_HEADSETHOOK KeyEvent, which is used to answer/hang up calls and
+     * play/stop media
+     * @hide
+     */
+    public static final int GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10;
+
     private static final String LOG_TAG = "AccessibilityService";
 
     /**
diff --git a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
index 9a3dad2..623734e 100644
--- a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java
@@ -30,7 +30,6 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Xml;
 
@@ -153,7 +152,7 @@
                     com.android.internal.R.styleable.AccessibilityShortcutTarget_settingsActivity);
             asAttributes.recycle();
 
-            if (mDescriptionResId == 0 || mSummaryResId == 0) {
+            if ((mDescriptionResId == 0 && mHtmlDescriptionRes == 0) || mSummaryResId == 0) {
                 throw new XmlPullParserException("No description or summary in meta-data");
             }
         } catch (PackageManager.NameNotFoundException e) {
@@ -243,7 +242,10 @@
     public String loadHtmlDescription(@NonNull PackageManager packageManager) {
         final String htmlDescription = loadResourceString(packageManager, mActivityInfo,
                 mHtmlDescriptionRes);
-        return TextUtils.isEmpty(htmlDescription) ? null : getFilteredHtmlText(htmlDescription);
+        if (htmlDescription != null) {
+            return getFilteredHtmlText(htmlDescription);
+        }
+        return null;
     }
 
     /**
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 8e0d939..37f683e 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -233,12 +233,9 @@
                 }
 
                 private void reportStackTraceIfNeeded(@NonNull SyncNotedAppOp op) {
-                    if (sConfig.getSampledOpCode() == OP_NONE
-                            && sConfig.getExpirationTimeSinceBootMillis()
-                            >= SystemClock.elapsedRealtime()) {
+                    if (!isCollectingStackTraces()) {
                         return;
                     }
-
                     MessageSamplingConfig config = sConfig;
                     if (leftCircularDistance(strOpToOp(op.getOp()), config.getSampledOpCode(),
                             _NUM_OP) <= config.getAcceptableLeftDistance()
@@ -8181,7 +8178,22 @@
      * @hide
      */
     public static boolean isListeningForOpNoted() {
-        return sOnOpNotedCallback != null;
+        return sOnOpNotedCallback != null || isCollectingStackTraces();
+    }
+
+    /**
+     * @return {@code true} iff the process is currently sampled for stacktrace collection.
+     *
+     * @see #setOnOpNotedCallback
+     *
+     * @hide
+     */
+    private static boolean isCollectingStackTraces() {
+        if (sConfig.getSampledOpCode() == OP_NONE &&
+                sConfig.getExpirationTimeSinceBootMillis() >= SystemClock.elapsedRealtime()) {
+            return false;
+        }
+        return true;
     }
 
     /**
diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl
index 1211c73..ed6ba0c 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -43,7 +43,7 @@
     @UnsupportedAppUsage
     void setAppInactive(String packageName, boolean inactive, int userId);
     @UnsupportedAppUsage
-    boolean isAppInactive(String packageName, int userId);
+    boolean isAppInactive(String packageName, int userId, String callingPackage);
     void onCarrierPrivilegedAppsChanged();
     void reportChooserSelection(String packageName, int userId, String contentType,
             in String[] annotations, String action);
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index 0a67802..2ce6a86 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -622,12 +622,17 @@
      * app hasn't been used directly or indirectly for a period of time defined by the system. This
      * could be of the order of several hours or days. Apps are not considered inactive when the
      * device is charging.
+     * <p> The caller must have {@link android.Manifest.permission#PACKAGE_USAGE_STATS} to query the
+     * inactive state of other apps</p>
+     *
      * @param packageName The package name of the app to query
-     * @return whether the app is currently considered inactive
+     * @return whether the app is currently considered inactive or false if querying another app
+     * without {@link android.Manifest.permission#PACKAGE_USAGE_STATS}
      */
     public boolean isAppInactive(String packageName) {
         try {
-            return mService.isAppInactive(packageName, mContext.getUserId());
+            return mService.isAppInactive(packageName, mContext.getUserId(),
+                    mContext.getOpPackageName());
         } catch (RemoteException e) {
             // fall through and return default
         }
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 1dadbda..85bafd9 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1695,7 +1695,7 @@
         }
 
         // Check to see if overlay should be excluded based on system property condition
-        if (!checkRequiredSystemProperty(requiredSystemPropertyName,
+        if (!checkRequiredSystemProperties(requiredSystemPropertyName,
                 requiredSystemPropertyValue)) {
             Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and "
                     + codePath + ": overlay ignored due to required system property: "
@@ -1997,7 +1997,7 @@
                 }
 
                 // check to see if overlay should be excluded based on system property condition
-                if (!checkRequiredSystemProperty(propName, propValue)) {
+                if (!checkRequiredSystemProperties(propName, propValue)) {
                     Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and "
                         + pkg.baseCodePath+ ": overlay ignored due to required system property: "
                         + propName + " with value: " + propValue);
@@ -2427,24 +2427,42 @@
 
     /**
      * Returns {@code true} if both the property name and value are empty or if the given system
-     * property is set to the specified value. In all other cases, returns {@code false}
+     * property is set to the specified value. Properties can be one or more, and if properties are
+     * more than one, they must be separated by comma, and count of names and values must be equal,
+     * and also every given system property must be set to the corresponding value.
+     * In all other cases, returns {@code false}
      */
-    public static boolean checkRequiredSystemProperty(String propName, String propValue) {
-        if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) {
-            if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) {
+    public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames,
+            @Nullable String rawPropValues) {
+        if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) {
+            if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) {
                 // malformed condition - incomplete
-                Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName
-                    + "=" + propValue + "' - require both requiredSystemPropertyName"
-                    + " AND requiredSystemPropertyValue to be specified.");
+                Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames
+                        + "=" + rawPropValues + "' - require both requiredSystemPropertyName"
+                        + " AND requiredSystemPropertyValue to be specified.");
                 return false;
             }
             // no valid condition set - so no exclusion criteria, overlay will be included.
             return true;
         }
 
-        // check property value - make sure it is both set and equal to expected value
-        final String currValue = SystemProperties.get(propName);
-        return (currValue != null && currValue.equals(propValue));
+        final String[] propNames = rawPropNames.split(",");
+        final String[] propValues = rawPropValues.split(",");
+
+        if (propNames.length != propValues.length) {
+            Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames
+                    + "=" + rawPropValues + "' - require both requiredSystemPropertyName"
+                    + " AND requiredSystemPropertyValue lists to have the same size.");
+            return false;
+        }
+        for (int i = 0; i < propNames.length; i++) {
+            // Check property value: make sure it is both set and equal to expected value
+            final String currValue = SystemProperties.get(propNames[i]);
+            if (!TextUtils.equals(currValue, propValues[i])) {
+                return false;
+            }
+        }
+        return true;
     }
 
     /**
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 27399e4..2f416a2 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -413,7 +413,7 @@
         }
 
         // Check to see if overlay should be excluded based on system property condition
-        if (!PackageParser.checkRequiredSystemProperty(requiredSystemPropertyName,
+        if (!PackageParser.checkRequiredSystemProperties(requiredSystemPropertyName,
                 requiredSystemPropertyValue)) {
             Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and "
                     + codePath + ": overlay ignored due to required system property: "
diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
index 29ece49..88f4c31 100644
--- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
@@ -84,7 +84,6 @@
 import android.os.Bundle;
 import android.os.FileUtils;
 import android.os.RemoteException;
-import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.ext.SdkExtensions;
 import android.text.TextUtils;
@@ -2348,7 +2347,7 @@
                     R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName);
             String propValue = sa.getString(
                     R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue);
-            if (!checkOverlayRequiredSystemProperty(propName, propValue)) {
+            if (!PackageParser.checkRequiredSystemProperties(propName, propValue)) {
                 Slog.i(TAG, "Skipping target and overlay pair " + target + " and "
                         + pkg.getBaseCodePath()
                         + ": overlay ignored due to required system property: "
@@ -2522,24 +2521,6 @@
         }
     }
 
-    private static boolean checkOverlayRequiredSystemProperty(String propName, String propValue) {
-        if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) {
-            if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) {
-                // malformed condition - incomplete
-                Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName
-                        + "=" + propValue + "' - require both requiredSystemPropertyName"
-                        + " AND requiredSystemPropertyValue to be specified.");
-                return false;
-            }
-            // no valid condition set - so no exclusion criteria, overlay will be included.
-            return true;
-        }
-
-        // check property value - make sure it is both set and equal to expected value
-        final String currValue = SystemProperties.get(propName);
-        return (currValue != null && currValue.equals(propValue));
-    }
-
     /**
      * This is a pre-density application which will get scaled - instead of being pixel perfect.
      * This type of application is not resizable.
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index ca86157..1fef071 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -897,6 +897,34 @@
             return -1;
         }
 
+        /**
+         * Scale all primitives of this effect.
+         *
+         * @param gamma the gamma adjustment to apply
+         * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and
+         *         MAX_AMPLITUDE
+         * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE
+         *
+         * @return A {@link Composed} effect with same but scaled primitives.
+         */
+        public Composed scale(float gamma, int maxAmplitude) {
+            if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) {
+                throw new IllegalArgumentException(
+                        "Amplitude is negative or greater than MAX_AMPLITUDE");
+            }
+            if (gamma == 1.0f && maxAmplitude == MAX_AMPLITUDE) {
+                // Just return a copy of the original if there's no scaling to be done.
+                return new Composed(mPrimitiveEffects);
+            }
+            List<Composition.PrimitiveEffect> scaledPrimitives = new ArrayList<>();
+            for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) {
+                float adjustedScale = MathUtils.pow(primitive.scale, gamma);
+                float newScale = adjustedScale * maxAmplitude / (float) MAX_AMPLITUDE;
+                scaledPrimitives.add(new Composition.PrimitiveEffect(
+                        primitive.id, newScale, primitive.delay));
+            }
+            return new Composed(scaledPrimitives);
+        }
 
         /**
          * @hide
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index aee32ed..a1a11ed 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -2447,7 +2447,12 @@
         final String filePath = path.getCanonicalPath();
         final StorageVolume volume = getStorageVolume(path);
         if (volume == null) {
-            throw new IllegalStateException("Failed to update quota type for " + filePath);
+            Log.w(TAG, "Failed to update quota type for " + filePath);
+            return;
+        }
+        if (!volume.isEmulated()) {
+            // We only support quota tracking on emulated filesystems
+            return;
         }
 
         final int userId = volume.getOwner().getIdentifier();
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index af91596..1f6555c 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -102,8 +102,8 @@
     private static final boolean DEFAULT_ALLOW_REMINDERS = false;
     private static final boolean DEFAULT_ALLOW_EVENTS = false;
     private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = true;
-    private static final boolean DEFAULT_ALLOW_CONV = true;
-    private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+    private static final boolean DEFAULT_ALLOW_CONV = false;
+    private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_NONE;
     private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false;
     private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = 0;
 
@@ -125,8 +125,8 @@
     private static final String ALLOW_ATT_EVENTS = "events";
     private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff";
     private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn";
-    private static final String ALLOW_ATT_CONV = "conv";
-    private static final String ALLOW_ATT_CONV_FROM = "convFrom";
+    private static final String ALLOW_ATT_CONV = "convos";
+    private static final String ALLOW_ATT_CONV_FROM = "convosFrom";
     private static final String DISALLOW_TAG = "disallow";
     private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects";
     private static final String STATE_TAG = "state";
diff --git a/core/java/android/view/IScrollCaptureClient.aidl b/core/java/android/view/IScrollCaptureClient.aidl
new file mode 100644
index 0000000..5f135a37
--- /dev/null
+++ b/core/java/android/view/IScrollCaptureClient.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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 android.view;
+
+import android.graphics.Rect;
+import android.view.Surface;
+
+
+ /**
+   * Interface implemented by a client of the Scroll Capture framework to receive requests
+   * to start, capture images and end the session.
+   *
+   * {@hide}
+   */
+interface IScrollCaptureClient {
+
+    /**
+     * Informs the client that it has been selected for scroll capture and should prepare to
+     * to begin handling capture requests.
+     */
+    oneway void startCapture(in Surface surface);
+
+    /**
+     * Request the client capture an image within the provided rectangle.
+     *
+     * @see android.view.ScrollCaptureCallback#onScrollCaptureRequest
+     */
+    oneway void requestImage(in Rect captureArea);
+
+    /**
+     * Inform the client that capture has ended. The client should shut down and release all
+     * local resources in use and prepare for return to normal interactive usage.
+     */
+    oneway void endCapture();
+}
diff --git a/core/java/android/view/IScrollCaptureController.aidl b/core/java/android/view/IScrollCaptureController.aidl
new file mode 100644
index 0000000..8474a00
--- /dev/null
+++ b/core/java/android/view/IScrollCaptureController.aidl
@@ -0,0 +1,63 @@
+/*
+ * 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 android.view;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Surface;
+
+import android.view.IScrollCaptureClient;
+
+/**
+ * Interface to a controller passed to the {@link IScrollCaptureClient} which provides the client an
+ * asynchronous callback channel for responses.
+ *
+ * {@hide}
+ */
+interface IScrollCaptureController {
+    /**
+     * Scroll capture is available, and a client connect has been returned.
+     *
+     * @param client interface to a ScrollCaptureCallback in the window process
+     * @param scrollAreaInWindow the location of scrolling in global (window) coordinate space
+     */
+    oneway void onClientConnected(in IScrollCaptureClient client, in Rect scrollBounds,
+            in Point positionInWindow);
+
+    /**
+     * Nothing in the window can be scrolled, scroll capture not offered.
+     */
+    oneway void onClientUnavailable();
+
+    /**
+     * Notifies the system that the client has confirmed the request and is ready to begin providing
+     * image requests.
+     */
+    oneway void onCaptureStarted();
+
+    /**
+     * Received a response from a capture request.
+     */
+    oneway void onCaptureBufferSent(long frameNumber, in Rect capturedArea);
+
+    /**
+     * Signals that the capture session has completed and the target window may be returned to
+     * normal interactive use. This may be due to normal shutdown, or after a timeout or other
+     * unrecoverable state change such as activity lifecycle, window visibility or focus.
+     */
+    oneway void onConnectionClosed();
+}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 9f2d36d..e09bf9d 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -21,15 +21,16 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
+import android.util.MergedConfiguration;
+import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.IScrollCaptureController;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.DisplayCutout;
-import android.view.InsetsState;
-import android.view.InsetsSourceControl;
 
 import com.android.internal.os.IResultReceiver;
-import android.util.MergedConfiguration;
 
 /**
  * API back to a client window that the Window Manager uses to inform it of
@@ -139,4 +140,11 @@
      * Tell the window that it is either gaining or losing pointer capture.
      */
     void dispatchPointerCaptureChanged(boolean hasCapture);
+
+    /**
+     * Called when Scroll Capture support is requested for a window.
+     *
+     * @param controller the controller to receive responses
+     */
+    void requestScrollCapture(in IScrollCaptureController controller);
 }
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index b0bacb9..b3b53f0 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -42,6 +42,7 @@
 import android.view.IDisplayWindowRotationController;
 import android.view.IOnKeyguardExitResult;
 import android.view.IPinnedStackListener;
+import android.view.IScrollCaptureController;
 import android.view.RemoteAnimationAdapter;
 import android.view.IRotationWatcher;
 import android.view.ISystemGestureExclusionListener;
@@ -749,4 +750,18 @@
      * @param flags see definition in SurfaceTracing.cpp
      */
     void setLayerTracingFlags(int flags);
+
+    /**
+     * Forwards a scroll capture request to the appropriate window, if available.
+     *
+     * @param displayId the id of the display to target
+     * @param behindClient token for a window, used to filter the search to windows behind it, or
+     *                     {@code null} to accept a window at any zOrder
+     * @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids
+     * @param controller the controller to receive results, a call to either
+     *      {@link IScrollCaptureController#onClientConnected} or
+     *      {@link IScrollCaptureController#onClientUnavailable}.
+     */
+    void requestScrollCapture(int displayId, IBinder behindClient, int taskId,
+            IScrollCaptureController controller);
 }
diff --git a/core/java/android/view/ScrollCaptureCallback.java b/core/java/android/view/ScrollCaptureCallback.java
new file mode 100644
index 0000000..e1a4e74
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureCallback.java
@@ -0,0 +1,151 @@
+/*
+ * 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 android.view;
+
+import android.annotation.NonNull;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+
+import java.util.function.Consumer;
+
+/**
+ * A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for
+ * the scroll capture system. A single callback is responsible for providing support to a single
+ * scrolling UI element. At request time, the system will select the best candidate from among all
+ * callbacks registered within the window.
+ * <p>
+ * A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as
+ * {@link Window#addScrollCaptureCallback}. The point where the callback is registered defines the
+ * frame of reference for the bounds measurements used.
+ * <p>
+ * <b>Terminology</b>
+ * <dl>
+ * <dt>Containing View</dt>
+ * <dd>The view on which this callback is attached, or the root view of the window if the callback
+ * is assigned  directly to a window.</dd>
+ *
+ * <dt>Scroll Bounds</dt>
+ * <dd>A rectangle which describes an area within the containing view where scrolling content may
+ * be positioned. This may be the Containing View bounds itself, or any rectangle within.
+ * Requested by {@link #onScrollCaptureSearch}.</dd>
+ *
+ * <dt>Scroll Delta</dt>
+ * <dd>The distance the scroll position has moved since capture started. Implementations are
+ * responsible for tracking changes in vertical scroll position during capture. This is required to
+ * map the capture area to the correct location, given the current scroll position.
+ *
+ * <dt>Capture Area</dt>
+ * <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical
+ * position remains relative to the starting scroll position and any movement since ("Scroll Delta")
+ * should be subtracted to locate the correct local position, and scrolled into view as necessary.
+ * </dd>
+ * </dl>
+ *
+ * @see View#setScrollCaptureHint(int)
+ * @see View#setScrollCaptureCallback(ScrollCaptureCallback)
+ * @see Window#addScrollCaptureCallback(ScrollCaptureCallback)
+ *
+ * @hide
+ */
+@UiThread
+public interface ScrollCaptureCallback {
+
+    /**
+     * The system is searching for the appropriate scrolling container to capture and would like to
+     * know the size and position of scrolling content handled by this callback.
+     * <p>
+     * Implementations should inset {@code containingViewBounds} to cover only the area within the
+     * containing view where scrolling content may be positioned. This should cover only the content
+     * which tracks with scrolling movement.
+     * <p>
+     * Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling
+     * content is not available to capture, a {@code null} rectangle may be returned, and this view
+     * will be excluded as the target for this request.
+     * <p>
+     * Responses received after XXXms will be discarded.
+     * <p>
+     * TODO: finalize timeout
+     *
+     * @param onReady              consumer for the updated rectangle
+     */
+    void onScrollCaptureSearch(@NonNull Consumer<Rect> onReady);
+
+    /**
+     * Scroll Capture has selected this callback to provide the scrolling image content.
+     * <p>
+     * The onReady signal should be called when ready to begin handling image requests.
+     */
+    void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady);
+
+    /**
+     * An image capture has been requested from the scrolling content.
+     * <p>
+     * <code>captureArea</code> contains the bounds of the image requested, relative to the
+     * rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as
+     * {@code scrollBounds}.
+     * here.
+     * <p>
+     * A series of requests will step by a constant vertical amount relative to {@code
+     * scrollBounds}, moving through the scrolling range of content, above and below the current
+     * visible area. The rectangle's vertical position will not account for any scrolling movement
+     * since capture started. Implementations therefore must track any scroll position changes and
+     * subtract this distance from requests.
+     * <p>
+     * To handle a request, the content should be scrolled to maximize the visible area of the
+     * requested rectangle. Offset {@code captureArea} again to account for any further scrolling.
+     * <p>
+     * Finally, clip this rectangle against scrollBounds to determine what portion, if any is
+     * visible content to capture. If the rectangle is completely clipped, set it to {@link
+     * Rect#setEmpty() empty} and skip the next step.
+     * <p>
+     * Make a copy of {@code captureArea}, transform to window coordinates and draw the window,
+     * clipped to this rectangle, into the {@link ScrollCaptureSession#getSurface() surface} at
+     * offset (0,0).
+     * <p>
+     * Finally, return the resulting {@code captureArea} using
+     * {@link ScrollCaptureSession#notifyBufferSent}.
+     * <p>
+     * If the response is not supplied within XXXms, the session will end with a call to {@link
+     * #onScrollCaptureEnd}, after which {@code session} is invalid and should be discarded.
+     * <p>
+     * TODO: finalize timeout
+     * <p>
+     *
+     * @param captureArea the area to capture, a rectangle within {@code scrollBounds}
+     */
+    void onScrollCaptureImageRequest(
+            @NonNull ScrollCaptureSession session, @NonNull Rect captureArea);
+
+    /**
+     * Signals that capture has ended. Implementations should release any temporary resources or
+     * references to objects in use during the capture. Any resources obtained from the session are
+     * now invalid and attempts to use them after this point may throw an exception.
+     * <p>
+     * The window should be returned as much as possible to its original state when capture started.
+     * At a minimum, the content should be scrolled to its original position.
+     * <p>
+     * <code>onReady</code> should be called when the window should be made visible and
+     * interactive. The system will wait up to XXXms for this call before proceeding.
+     * <p>
+     * TODO: finalize timeout
+     *
+     * @param onReady a callback to inform the system that the application has completed any
+     *                cleanup and is ready to become visible
+     */
+    void onScrollCaptureEnd(@NonNull Runnable onReady);
+}
+
diff --git a/core/java/android/view/ScrollCaptureClient.java b/core/java/android/view/ScrollCaptureClient.java
new file mode 100644
index 0000000..f163124
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureClient.java
@@ -0,0 +1,312 @@
+/*
+ * 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 android.view;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.annotation.WorkerThread;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A client of the system providing Scroll Capture capability on behalf of a Window.
+ * <p>
+ * An instance is created to wrap the selected {@link ScrollCaptureCallback}.
+ *
+ * @hide
+ */
+public class ScrollCaptureClient extends IScrollCaptureClient.Stub {
+
+    private static final String TAG = "ScrollCaptureClient";
+    private static final int DEFAULT_TIMEOUT = 1000;
+
+    private final Handler mHandler;
+    private ScrollCaptureTarget mSelectedTarget;
+    private int mTimeoutMillis = DEFAULT_TIMEOUT;
+
+    protected Surface mSurface;
+    private IScrollCaptureController mController;
+
+    private final Rect mScrollBounds;
+    private final Point mPositionInWindow;
+    private final CloseGuard mCloseGuard;
+
+    // The current session instance in use by the callback.
+    private ScrollCaptureSession mSession;
+
+    // Helps manage timeout callbacks registered to handler and aids testing.
+    private DelayedAction mTimeoutAction;
+
+    /**
+     * Constructs a ScrollCaptureClient.
+     *
+     * @param selectedTarget  the target the client is controlling
+     * @param controller the callbacks to reply to system requests
+     *
+     * @hide
+     */
+    public ScrollCaptureClient(
+            @NonNull ScrollCaptureTarget selectedTarget,
+            @NonNull IScrollCaptureController controller) {
+        requireNonNull(selectedTarget, "<selectedTarget> must non-null");
+        requireNonNull(controller, "<controller> must non-null");
+        final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(),
+                "target.getScrollBounds() must be non-null to construct a client");
+
+        mSelectedTarget = selectedTarget;
+        mHandler = selectedTarget.getContainingView().getHandler();
+        mScrollBounds = new Rect(scrollBounds);
+        mPositionInWindow = new Point(selectedTarget.getPositionInWindow());
+
+        mController = controller;
+        mCloseGuard = new CloseGuard();
+        mCloseGuard.open("close");
+
+        selectedTarget.getContainingView().addOnAttachStateChangeListener(
+                new View.OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {
+
+                    }
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        selectedTarget.getContainingView().removeOnAttachStateChangeListener(this);
+                        endCapture();
+                    }
+                });
+    }
+
+    @VisibleForTesting
+    public void setTimeoutMillis(int timeoutMillis) {
+        mTimeoutMillis = timeoutMillis;
+    }
+
+    @Nullable
+    @VisibleForTesting
+    public DelayedAction getTimeoutAction() {
+        return mTimeoutAction;
+    }
+
+    private void checkConnected() {
+        if (mSelectedTarget == null || mController == null) {
+            throw new IllegalStateException("This client has been disconnected.");
+        }
+    }
+
+    private void checkStarted() {
+        if (mSession == null) {
+            throw new IllegalStateException("Capture session has not been started!");
+        }
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void startCapture(Surface surface) throws RemoteException {
+        checkConnected();
+        mSurface = surface;
+        scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout);
+        mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this);
+        mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession,
+                this::onStartCaptureCompleted));
+    }
+
+    @UiThread
+    private void onStartCaptureCompleted() {
+        if (cancelTimeout()) {
+            mHandler.post(() -> {
+                try {
+                    mController.onCaptureStarted();
+                } catch (RemoteException e) {
+                    doShutdown();
+                }
+            });
+        }
+    }
+
+    @UiThread
+    private void onStartCaptureTimeout() {
+        endCapture();
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void requestImage(Rect requestRect) {
+        checkConnected();
+        checkStarted();
+        scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout);
+        // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted
+        mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest(
+                mSession, new Rect(requestRect)));
+    }
+
+    @UiThread
+    void onRequestImageCompleted(long frameNumber, Rect capturedArea) {
+        final Rect finalCapturedArea = new Rect(capturedArea);
+        if (cancelTimeout()) {
+            mHandler.post(() -> {
+                try {
+                    mController.onCaptureBufferSent(frameNumber, finalCapturedArea);
+                } catch (RemoteException e) {
+                    doShutdown();
+                }
+            });
+        }
+    }
+
+    @UiThread
+    private void onRequestImageTimeout() {
+        endCapture();
+    }
+
+    @WorkerThread // IScrollCaptureClient
+    @Override
+    public void endCapture() {
+        if (isStarted()) {
+            scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout);
+            mHandler.post(() ->
+                    mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted));
+        } else {
+            disconnect();
+        }
+    }
+
+    private boolean isStarted() {
+        return mController != null && mSelectedTarget != null;
+    }
+
+    @UiThread
+    private void onEndCaptureCompleted() { // onEndCaptureCompleted
+        if (cancelTimeout()) {
+            doShutdown();
+        }
+    }
+
+    @UiThread
+    private void onEndCaptureTimeout() {
+        doShutdown();
+    }
+
+
+    private void doShutdown() {
+        try {
+            if (mController != null) {
+                mController.onConnectionClosed();
+            }
+        } catch (RemoteException e) {
+            // Ignore
+        } finally {
+            disconnect();
+        }
+    }
+
+    /**
+     * Shuts down this client and releases references to dependent objects. No attempt is made
+     * to notify the controller, use with caution!
+     */
+    public void disconnect() {
+        if (mSession != null) {
+            mSession.disconnect();
+            mSession = null;
+        }
+
+        mSelectedTarget = null;
+        mController = null;
+    }
+
+    /** @return a string representation of the state of this client */
+    public String toString() {
+        return "ScrollCaptureClient{"
+                + ", session=" + mSession
+                + ", selectedTarget=" + mSelectedTarget
+                + ", clientCallbacks=" + mController
+                + "}";
+    }
+
+    private boolean cancelTimeout() {
+        if (mTimeoutAction != null) {
+            return mTimeoutAction.cancel();
+        }
+        return false;
+    }
+
+    private void scheduleTimeout(long timeoutMillis, Runnable action) {
+        if (mTimeoutAction != null) {
+            mTimeoutAction.cancel();
+        }
+        mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action);
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static class DelayedAction {
+        private final AtomicBoolean mCompleted = new AtomicBoolean();
+        private final Object mToken = new Object();
+        private final Handler mHandler;
+        private final Runnable mAction;
+
+        @VisibleForTesting
+        public DelayedAction(Handler handler, long timeoutMillis, Runnable action) {
+            mHandler = handler;
+            mAction = action;
+            mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis);
+        }
+
+        private boolean onTimeout() {
+            if (mCompleted.compareAndSet(false, true)) {
+                mAction.run();
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Cause the timeout action to run immediately and mark as timed out.
+         *
+         * @return true if the timeout was run, false if the timeout had already been canceled
+         */
+        @VisibleForTesting
+        public boolean timeoutNow() {
+            return onTimeout();
+        }
+
+        /**
+         * Attempt to cancel the timeout action (such as after a callback is made)
+         *
+         * @return true if the timeout was canceled and will not run, false if time has expired and
+         * the timeout action has or will run momentarily
+         */
+        public boolean cancel() {
+            if (!mCompleted.compareAndSet(false, true)) {
+                // Whoops, too late!
+                return false;
+            }
+            mHandler.removeCallbacksAndMessages(mToken);
+            return true;
+        }
+    }
+}
diff --git a/core/java/android/view/ScrollCaptureSession.java b/core/java/android/view/ScrollCaptureSession.java
new file mode 100644
index 0000000..628e23f
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureSession.java
@@ -0,0 +1,105 @@
+/*
+ * 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 android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+/**
+ * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the
+ * system during an active scroll capture operation. During the scope of a session, a callback
+ * will receive a series of requests for image data. Resources provided here are valid for use
+ * until {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable)}.
+ *
+ * @hide
+ */
+public class ScrollCaptureSession {
+
+    private final Surface mSurface;
+    private final Rect mScrollBounds;
+    private final Point mPositionInWindow;
+
+    @Nullable
+    private ScrollCaptureClient mClient;
+
+    /** @hide */
+    public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow,
+            ScrollCaptureClient client) {
+        mSurface = surface;
+        mScrollBounds = scrollBounds;
+        mPositionInWindow = positionInWindow;
+        mClient = client;
+    }
+
+    /**
+     * Returns a
+     * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the
+     * form of a {@link Surface} for transfer of image buffers.
+     *
+     * @return the surface for transferring image buffers
+     * @throws IllegalStateException if the session has been closed
+     */
+    @NonNull
+    public Surface getSurface() {
+        return mSurface;
+    }
+
+    /**
+     * Returns the {@code scroll bounds}, as provided by
+     * {@link ScrollCaptureCallback#onScrollCaptureSearch}.
+     *
+     * @return the area of scrolling content within the containing view
+     */
+    @NonNull
+    public Rect getScrollBounds() {
+        return mScrollBounds;
+    }
+
+    /**
+     * Returns the offset of {@code scroll bounds} within the window.
+     *
+     * @return the area of scrolling content within the containing view
+     */
+    @NonNull
+    public Point getPositionInWindow() {
+        return mPositionInWindow;
+    }
+
+    /**
+     * Notify the system that an a buffer has been posted via the getSurface() channel.
+     *
+     * @param frameNumber  the frame number of the queued buffer
+     * @param capturedArea the area captured, relative to scroll bounds
+     */
+    public void notifyBufferSent(long frameNumber, @NonNull Rect capturedArea) {
+        if (mClient != null) {
+            mClient.onRequestImageCompleted(frameNumber, capturedArea);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void disconnect() {
+        mClient = null;
+        if (mSurface.isValid()) {
+            mSurface.release();
+        }
+    }
+}
diff --git a/core/java/android/view/ScrollCaptureTarget.java b/core/java/android/view/ScrollCaptureTarget.java
new file mode 100644
index 0000000..f3fcabb
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureTarget.java
@@ -0,0 +1,135 @@
+/*
+ * 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 android.view;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+
+import com.android.internal.util.FastMath;
+
+/**
+ * A target collects the set of contextual information for a ScrollCaptureHandler discovered during
+ * a {@link View#dispatchScrollCaptureSearch scroll capture search}.
+ *
+ * @hide
+ */
+public final class ScrollCaptureTarget {
+    private final View mContainingView;
+    private final ScrollCaptureCallback mCallback;
+    private final Rect mLocalVisibleRect;
+    private final Point mPositionInWindow;
+    private final int mHint;
+    private Rect mScrollBounds;
+
+    private final float[] mTmpFloatArr = new float[2];
+    private final Matrix mMatrixViewLocalToWindow = new Matrix();
+    private final Rect mTmpRect = new Rect();
+
+    public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect,
+            @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) {
+        mContainingView = scrollTarget;
+        mHint = mContainingView.getScrollCaptureHint();
+        mCallback = callback;
+        mLocalVisibleRect = localVisibleRect;
+        mPositionInWindow = positionInWindow;
+    }
+
+    /** @return the hint that the {@code containing view} had during the scroll capture search */
+    @View.ScrollCaptureHint
+    public int getHint() {
+        return mHint;
+    }
+
+    /** @return the {@link ScrollCaptureCallback} for this target */
+    @NonNull
+    public ScrollCaptureCallback getCallback() {
+        return mCallback;
+    }
+
+    /** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */
+    @NonNull
+    public View getContainingView() {
+        return mContainingView;
+    }
+
+    /**
+     * Returns the un-clipped, visible bounds of the containing view during the scroll capture
+     * search. This is used to determine on-screen area to assist in selecting the primary target.
+     *
+     * @return the visible bounds of the {@code containing view} in view-local coordinates
+     */
+    @NonNull
+    public Rect getLocalVisibleRect() {
+        return mLocalVisibleRect;
+    }
+
+    /** @return the position of the {@code containing view} within the window */
+    @NonNull
+    public Point getPositionInWindow() {
+        return mPositionInWindow;
+    }
+
+    /** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */
+    @Nullable
+    public Rect getScrollBounds() {
+        return mScrollBounds;
+    }
+
+    /**
+     * Sets the scroll bounds rect to the intersection of provided rect and the current bounds of
+     * the {@code containing view}.
+     */
+    public void setScrollBounds(@Nullable Rect scrollBounds) {
+        mScrollBounds = Rect.copyOrNull(scrollBounds);
+        if (mScrollBounds == null) {
+            return;
+        }
+        if (!mScrollBounds.intersect(0, 0,
+                mContainingView.getWidth(), mContainingView.getHeight())) {
+            mScrollBounds.setEmpty();
+        }
+    }
+
+    private static void zero(float[] pointArray) {
+        pointArray[0] = 0;
+        pointArray[1] = 0;
+    }
+
+    private static void roundIntoPoint(Point pointObj, float[] pointArray) {
+        pointObj.x = FastMath.round(pointArray[0]);
+        pointObj.y = FastMath.round(pointArray[1]);
+    }
+
+    /**
+     * Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the
+     * current state of the {@code containing view}.
+     */
+    @UiThread
+    public void updatePositionInWindow() {
+        mMatrixViewLocalToWindow.reset();
+        mContainingView.transformMatrixToGlobal(mMatrixViewLocalToWindow);
+
+        zero(mTmpFloatArr);
+        mMatrixViewLocalToWindow.mapPoints(mTmpFloatArr);
+        roundIntoPoint(mPositionInWindow, mTmpFloatArr);
+    }
+
+}
diff --git a/core/java/android/view/ScrollCaptureTargetResolver.java b/core/java/android/view/ScrollCaptureTargetResolver.java
new file mode 100644
index 0000000..71e82c5
--- /dev/null
+++ b/core/java/android/view/ScrollCaptureTargetResolver.java
@@ -0,0 +1,387 @@
+/*
+ * 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 android.view;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Queries additional state from a list of {@link ScrollCaptureTarget targets} via asynchronous
+ * callbacks, then aggregates and reduces the target list to a single target, or null if no target
+ * is suitable.
+ * <p>
+ * The rules for selection are (in order):
+ * <ul>
+ * <li>prefer getScrollBounds(): non-empty
+ * <li>prefer View.getScrollCaptureHint == SCROLL_CAPTURE_HINT_INCLUDE
+ * <li>prefer descendants before parents
+ * <li>prefer larger area for getScrollBounds() (clipped to view bounds)
+ * </ul>
+ *
+ * <p>
+ * All calls to {@link ScrollCaptureCallback#onScrollCaptureSearch} are made on the main thread,
+ * with results are queued and consumed to the main thread as well.
+ *
+ * @see #start(Handler, long, Consumer)
+ *
+ * @hide
+ */
+@UiThread
+public class ScrollCaptureTargetResolver {
+    private static final String TAG = "ScrollCaptureTargetRes";
+    private static final boolean DEBUG = true;
+
+    private final Object mLock = new Object();
+
+    private final Queue<ScrollCaptureTarget> mTargets;
+    private Handler mHandler;
+    private long mTimeLimitMillis;
+
+    private Consumer<ScrollCaptureTarget> mWhenComplete;
+    private int mPendingBoundsRequests;
+    private long mDeadlineMillis;
+
+    private ScrollCaptureTarget mResult;
+    private boolean mFinished;
+
+    private boolean mStarted;
+
+    private static int area(Rect r) {
+        return r.width() * r.height();
+    }
+
+    private static boolean nullOrEmpty(Rect r) {
+        return r == null || r.isEmpty();
+    }
+
+    /**
+     * Binary operator which selects the best {@link ScrollCaptureTarget}.
+     */
+    private static ScrollCaptureTarget chooseTarget(ScrollCaptureTarget a, ScrollCaptureTarget b) {
+        Log.d(TAG, "chooseTarget: " + a + " or " + b);
+        // Nothing plus nothing is still nothing.
+        if (a == null && b == null) {
+            Log.d(TAG, "chooseTarget: (both null) return " + null);
+            return null;
+        }
+        // Prefer non-null.
+        if (a == null || b == null) {
+            ScrollCaptureTarget c = (a == null) ? b : a;
+            Log.d(TAG, "chooseTarget: (other is null) return " + c);
+            return c;
+
+        }
+
+        boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds());
+        boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds());
+        if (emptyScrollBoundsA || emptyScrollBoundsB) {
+            if (emptyScrollBoundsA && emptyScrollBoundsB) {
+                // Both have an empty or null scrollBounds
+                Log.d(TAG, "chooseTarget: (both have empty or null bounds) return " + null);
+                return null;
+            }
+            // Prefer the one with a non-empty scroll bounds
+            if (emptyScrollBoundsA) {
+                Log.d(TAG, "chooseTarget: (a has empty or null bounds) return " + b);
+                return b;
+            }
+            Log.d(TAG, "chooseTarget: (b has empty or null bounds) return " + a);
+            return a;
+        }
+
+        final View viewA = a.getContainingView();
+        final View viewB = b.getContainingView();
+
+        // Prefer any view with scrollCaptureHint="INCLUDE", over one without
+        // This is an escape hatch for the next rule (descendants first)
+        boolean hintIncludeA = hasIncludeHint(viewA);
+        boolean hintIncludeB = hasIncludeHint(viewB);
+        if (hintIncludeA != hintIncludeB) {
+            ScrollCaptureTarget c = (hintIncludeA) ? a : b;
+            Log.d(TAG, "chooseTarget: (has hint=INCLUDE) return " + c);
+            return c;
+        }
+
+        // If the views are relatives, prefer the descendant. This allows implementations to
+        // leverage nested scrolling APIs by interacting with the innermost scrollable view (as
+        // would happen with touch input).
+        if (isDescendant(viewA, viewB)) {
+            Log.d(TAG, "chooseTarget: (b is descendant of a) return " + b);
+            return b;
+        }
+        if (isDescendant(viewB, viewA)) {
+            Log.d(TAG, "chooseTarget: (a is descendant of b) return " + a);
+            return a;
+        }
+
+        // finally, prefer one with larger scroll bounds
+        int scrollAreaA = area(a.getScrollBounds());
+        int scrollAreaB = area(b.getScrollBounds());
+        ScrollCaptureTarget c = (scrollAreaA >= scrollAreaB) ? a : b;
+        Log.d(TAG, "chooseTarget: return " + c);
+        return c;
+    }
+
+    /**
+     * Creates an instance to query and filter {@code target}.
+     *
+     * @param targets   a list of {@link ScrollCaptureTarget} as collected by {@link
+     *                  View#dispatchScrollCaptureSearch}.
+     * @param uiHandler the UI thread handler for the view tree
+     * @see #start(long, Consumer)
+     */
+    public ScrollCaptureTargetResolver(Queue<ScrollCaptureTarget> targets) {
+        mTargets = targets;
+    }
+
+    void checkThread() {
+        if (mHandler.getLooper() != Looper.myLooper()) {
+            throw new IllegalStateException("Called from wrong thread! ("
+                    + Thread.currentThread().getName() + ")");
+        }
+    }
+
+    /**
+     * Blocks until a result is returned (after completion or timeout).
+     * <p>
+     * For testing only. Normal usage should receive a callback after calling {@link #start}.
+     */
+    @VisibleForTesting
+    public ScrollCaptureTarget waitForResult() throws InterruptedException {
+        synchronized (mLock) {
+            while (!mFinished) {
+                mLock.wait();
+            }
+        }
+        return mResult;
+    }
+
+
+    private void supplyResult(ScrollCaptureTarget target) {
+        checkThread();
+        if (mFinished) {
+            return;
+        }
+        mResult = chooseTarget(mResult, target);
+        boolean finish = mPendingBoundsRequests == 0
+                || SystemClock.elapsedRealtime() >= mDeadlineMillis;
+        if (finish) {
+            System.err.println("We think we're done, or timed out");
+            mPendingBoundsRequests = 0;
+            mWhenComplete.accept(mResult);
+            synchronized (mLock) {
+                mFinished = true;
+                mLock.notify();
+            }
+            mWhenComplete = null;
+        }
+    }
+
+    /**
+     * Asks all targets for {@link ScrollCaptureCallback#onScrollCaptureSearch(Consumer)
+     * scrollBounds}, and selects the primary target according to the {@link
+     * #chooseTarget} function.
+     *
+     * @param timeLimitMillis the amount of time to wait for all responses before delivering the top
+     *                        result
+     * @param resultConsumer  the consumer to receive the primary target
+     */
+    @AnyThread
+    public void start(Handler uiHandler, long timeLimitMillis,
+            Consumer<ScrollCaptureTarget> resultConsumer) {
+        synchronized (mLock) {
+            if (mStarted) {
+                throw new IllegalStateException("already started!");
+            }
+            if (timeLimitMillis < 0) {
+                throw new IllegalArgumentException("Time limit must be positive");
+            }
+            mHandler = uiHandler;
+            mTimeLimitMillis = timeLimitMillis;
+            mWhenComplete = resultConsumer;
+            if (mTargets.isEmpty()) {
+                mHandler.post(() -> supplyResult(null));
+                return;
+            }
+            mStarted = true;
+            uiHandler.post(() -> run(timeLimitMillis, resultConsumer));
+        }
+    }
+
+
+    private void run(long timeLimitMillis, Consumer<ScrollCaptureTarget> resultConsumer) {
+        checkThread();
+
+        mPendingBoundsRequests = mTargets.size();
+        for (ScrollCaptureTarget target : mTargets) {
+            queryTarget(target);
+        }
+        mDeadlineMillis = SystemClock.elapsedRealtime() + mTimeLimitMillis;
+        mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis);
+    }
+
+    private final Runnable mTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            checkThread();
+            supplyResult(null);
+        }
+    };
+
+
+    /**
+     * Adds a target to the list and requests {@link ScrollCaptureCallback#onScrollCaptureSearch}
+     * scrollBounds} from it. Results are returned by a call to {@link #onScrollBoundsProvided}.
+     *
+     * @param target the target to add
+     */
+    @UiThread
+    private void queryTarget(@NonNull ScrollCaptureTarget target) {
+        checkThread();
+        final ScrollCaptureCallback callback = target.getCallback();
+        // from the UI thread, request scroll bounds
+        callback.onScrollCaptureSearch(
+                // allow only one callback to onReady.accept():
+                new SingletonConsumer<Rect>(
+                        // Queue and consume on the UI thread
+                        ((scrollBounds) -> mHandler.post(
+                                () -> onScrollBoundsProvided(target, scrollBounds)))));
+
+    }
+
+    @UiThread
+    private void onScrollBoundsProvided(ScrollCaptureTarget target, @Nullable Rect scrollBounds) {
+        checkThread();
+        if (mFinished) {
+            return;
+        }
+
+        // Record progress.
+        mPendingBoundsRequests--;
+
+        // Remove the timeout.
+        mHandler.removeCallbacks(mTimeoutRunnable);
+
+        boolean doneOrTimedOut = mPendingBoundsRequests == 0
+                || SystemClock.elapsedRealtime() >= mDeadlineMillis;
+
+        final View containingView = target.getContainingView();
+        if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) {
+            target.updatePositionInWindow();
+            target.setScrollBounds(scrollBounds);
+            supplyResult(target);
+        }
+
+        System.err.println("mPendingBoundsRequests: " + mPendingBoundsRequests);
+        System.err.println("mDeadlineMillis: " + mDeadlineMillis);
+        System.err.println("SystemClock.elapsedRealtime(): " + SystemClock.elapsedRealtime());
+
+        if (!mFinished) {
+            // Reschedule the timeout.
+            System.err.println(
+                    "We think we're NOT done yet and will check back at " + mDeadlineMillis);
+            mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis);
+        }
+    }
+
+    private static boolean hasIncludeHint(View view) {
+        return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0;
+    }
+
+    /**
+     * Determines if {@code otherView} is a descendant of {@code view}.
+     *
+     * @param view      a view
+     * @param otherView another view
+     * @return true if {@code view} is an ancestor of {@code otherView}
+     */
+    private static boolean isDescendant(@NonNull View view, @NonNull View otherView) {
+        if (view == otherView) {
+            return false;
+        }
+        ViewParent otherParent = otherView.getParent();
+        while (otherParent != view && otherParent != null) {
+            otherParent = otherParent.getParent();
+        }
+        return otherParent == view;
+    }
+
+    private static int findRelation(@NonNull View a, @NonNull View b) {
+        if (a == b) {
+            return 0;
+        }
+
+        ViewParent parentA = a.getParent();
+        ViewParent parentB = b.getParent();
+
+        while (parentA != null || parentB != null) {
+            if (parentA == parentB) {
+                return 0;
+            }
+            if (parentA == b) {
+                return 1; // A is descendant of B
+            }
+            if (parentB == a) {
+                return -1; // B is descendant of A
+            }
+            if (parentA != null) {
+                parentA = parentA.getParent();
+            }
+            if (parentB != null) {
+                parentB = parentB.getParent();
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * A safe wrapper for a consumer callbacks intended to accept a single value. It ensures
+     * that the receiver of the consumer does not retain a reference to {@code target} after use nor
+     * cause race conditions by invoking {@link Consumer#accept accept} more than once.
+     *
+     * @param target the target consumer
+     */
+    static class SingletonConsumer<T> implements Consumer<T> {
+        final AtomicReference<Consumer<T>> mAtomicRef;
+
+        SingletonConsumer(Consumer<T> target) {
+            mAtomicRef = new AtomicReference<>(target);
+        }
+
+        @Override
+        public void accept(T t) {
+            final Consumer<T> consumer = mAtomicRef.getAndSet(null);
+            if (consumer != null) {
+                consumer.accept(t);
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 8abe72f..f98c1f6 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -144,6 +144,7 @@
 
 import com.android.internal.R;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.view.ScrollCaptureInternal;
 import com.android.internal.view.TooltipPopup;
 import com.android.internal.view.menu.MenuBuilder;
 import com.android.internal.widget.ScrollBarUtils;
@@ -167,6 +168,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Queue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
@@ -1311,7 +1313,6 @@
      */
     public static final int AUTOFILL_TYPE_LIST = 3;
 
-
     /**
      * Autofill type for a field that contains a date, which is represented by a long representing
      * the number of milliseconds since the standard base time known as "the epoch", namely
@@ -1441,6 +1442,58 @@
      */
     public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8;
 
+    /** {@hide} */
+    @IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"},
+            value = {
+                    SCROLL_CAPTURE_HINT_AUTO,
+                    SCROLL_CAPTURE_HINT_EXCLUDE,
+                    SCROLL_CAPTURE_HINT_INCLUDE,
+                    SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ScrollCaptureHint {}
+
+    /**
+     * The content of this view will be considered for scroll capture if scrolling is possible.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_AUTO = 0;
+
+    /**
+     * Explicitly exclcude this view as a potential scroll capture target. The system will not
+     * consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag
+     * takes precedence over.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1;
+
+    /**
+     * Explicitly include this view as a potential scroll capture target. When locating a scroll
+     * capture target, this view will be prioritized before others without this flag. Mutually
+     * exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2;
+
+    /**
+     * Explicitly exclude all children of this view as potential scroll capture targets. This view
+     * is unaffected. Note: Excluded children are not considered, regardless of {@link
+     * #SCROLL_CAPTURE_HINT_INCLUDE}.
+     *
+     * @see #getScrollCaptureHint()
+     * @see #setScrollCaptureHint(int)
+     * @hide
+     */
+    public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4;
 
     /**
      * This view is enabled. Interpretation varies by subclass.
@@ -3430,6 +3483,7 @@
      *                         11       PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK
      *                        1         PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS
      *                       1          PFLAG4_AUTOFILL_HIDE_HIGHLIGHT
+     *                     11           PFLAG4_SCROLL_CAPTURE_HINT_MASK
      * |-------|-------|-------|-------|
      */
 
@@ -3477,6 +3531,15 @@
      */
     private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200;
 
+    /**
+     * Shift for the bits in {@link #mPrivateFlags4} related to scroll capture.
+     */
+    static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10;
+
+    static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE
+            | SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS)
+            << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+
     /* End of masks for mPrivateFlags4 */
 
     /** @hide */
@@ -4690,6 +4753,11 @@
          * Used to track {@link #mSystemGestureExclusionRects}
          */
         public RenderNode.PositionUpdateListener mPositionUpdateListener;
+
+        /**
+         * Allows the application to implement custom scroll capture support.
+         */
+        ScrollCaptureCallback mScrollCaptureCallback;
     }
 
     @UnsupportedAppUsage
@@ -5941,6 +6009,9 @@
                 case R.styleable.View_forceDarkAllowed:
                     mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
                     break;
+                case R.styleable.View_scrollCaptureHint:
+                    setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO)));
+                    break;
             }
         }
 
@@ -29091,6 +29162,11 @@
         int mLeashedParentAccessibilityViewId;
 
         /**
+         *
+         */
+        ScrollCaptureInternal mScrollCaptureInternal;
+
+        /**
          * Creates a new set of attachment information with the specified
          * events handler and thread.
          *
@@ -29150,6 +29226,14 @@
 
             return events;
         }
+
+        @Nullable
+        ScrollCaptureInternal getScrollCaptureInternal() {
+            if (mScrollCaptureInternal != null) {
+                mScrollCaptureInternal = new ScrollCaptureInternal();
+            }
+            return mScrollCaptureInternal;
+        }
     }
 
     /**
@@ -29683,6 +29767,104 @@
         }
     }
 
+
+    /**
+     * Returns the current scroll capture hint for this view.
+     *
+     * @return the current scroll capture hint
+     *
+     * @hide
+     */
+    @ScrollCaptureHint
+    public int getScrollCaptureHint() {
+        return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK)
+                >> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT;
+    }
+
+    /**
+     * Sets the scroll capture hint for this View. These flags affect the search for a potential
+     * scroll capture targets.
+     *
+     * @param hint the scrollCaptureHint flags value to set
+     *
+     * @hide
+     */
+    public void setScrollCaptureHint(@ScrollCaptureHint int hint) {
+        mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK;
+        mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT)
+                & PFLAG4_SCROLL_CAPTURE_HINT_MASK);
+    }
+
+    /**
+     * Sets the callback to receive scroll capture requests. This component is the adapter between
+     * the scroll capture API and application UI code. If no callback is set, the system may provide
+     * an implementation. Any value provided here will take precedence over a system version.
+     * <p>
+     * This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link
+     * #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set.
+     * <p>
+     * It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when
+     * setting a custom callback to help ensure it is selected as the target.
+     *
+     * @param callback the new callback to assign
+     *
+     * @hide
+     */
+    public void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) {
+        getListenerInfo().mScrollCaptureCallback = callback;
+    }
+
+    /** {@hide} */
+    @Nullable
+    public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect,
+            @NonNull Point windowOffset) {
+        if (mAttachInfo == null) {
+            return null;
+        }
+        if (mAttachInfo.mScrollCaptureInternal == null) {
+            mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal();
+        }
+        return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect,
+                windowOffset);
+    }
+
+    /**
+     * Called when scroll capture is requested, to search for appropriate content to scroll. If
+     * applicable, this view adds itself to the provided list for consideration, subject to the
+     * flags set by {@link #setScrollCaptureHint}.
+     *
+     * @param localVisibleRect the local visible rect of this view
+     * @param windowOffset     the offset of localVisibleRect within the window
+     * @param targets          a queue which collects potential targets
+     *
+     * @throws IllegalStateException if this view is not attached to a window
+     * @hide
+     */
+    public void dispatchScrollCaptureSearch(@NonNull Rect localVisibleRect,
+            @NonNull Point windowOffset, @NonNull Queue<ScrollCaptureTarget> targets) {
+        int hint = getScrollCaptureHint();
+        if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) {
+            return;
+        }
+
+        // Get a callback provided by the framework, library or application.
+        ScrollCaptureCallback callback =
+                (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback;
+
+        // Try internal support for standard scrolling containers.
+        if (callback == null) {
+            callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset);
+        }
+
+        // If found, then add it to the list.
+        if (callback != null) {
+            // Add to the list for consideration
+            Point offset = new Point(windowOffset.x, windowOffset.y);
+            Rect rect = new Rect(localVisibleRect);
+            targets.add(new ScrollCaptureTarget(this, rect, offset, callback));
+        }
+    }
+
     /**
      * Dump all private flags in readable format, useful for documentation and
      * sanity checking.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index e34e84c..7935eb1 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -40,6 +40,7 @@
 import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -75,6 +76,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.function.Predicate;
 
 /**
@@ -188,7 +190,16 @@
     private PointF mLocalPoint;
 
     // Lazily-created holder for point computations.
-    private float[] mTempPoint;
+    private float[] mTempPosition;
+
+    // Lazily-created holder for point computations.
+    private Point mTempPoint;
+
+    // Lazily created Rect for dispatch to children
+    private Rect mTempRect;
+
+    // Lazily created int[2] for dispatch to children
+    private int[] mTempLocation;
 
     // Layout animation
     private LayoutAnimationController mLayoutAnimationController;
@@ -1860,7 +1871,7 @@
         final float tx = mCurrentDragStartEvent.mX;
         final float ty = mCurrentDragStartEvent.mY;
 
-        final float[] point = getTempPoint();
+        final float[] point = getTempLocationF();
         point[0] = tx;
         point[1] = ty;
         transformPointToViewLocal(point, child);
@@ -2932,9 +2943,23 @@
         }
     }
 
-    private float[] getTempPoint() {
+    private Rect getTempRect() {
+        if (mTempRect == null) {
+            mTempRect = new Rect();
+        }
+        return mTempRect;
+    }
+
+    private float[] getTempLocationF() {
+        if (mTempPosition == null) {
+            mTempPosition = new float[2];
+        }
+        return mTempPosition;
+    }
+
+    private Point getTempPoint() {
         if (mTempPoint == null) {
-            mTempPoint = new float[2];
+            mTempPoint = new Point();
         }
         return mTempPoint;
     }
@@ -2948,7 +2973,7 @@
     @UnsupportedAppUsage
     protected boolean isTransformedTouchPointInView(float x, float y, View child,
             PointF outLocalPoint) {
-        final float[] point = getTempPoint();
+        final float[] point = getTempLocationF();
         point[0] = x;
         point[1] = y;
         transformPointToViewLocal(point, child);
@@ -4568,7 +4593,7 @@
             final boolean nonActionable = !child.isClickable() && !child.isLongClickable();
             final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0;
             if (nonActionable || duplicatesState) {
-                final float[] point = getTempPoint();
+                final float[] point = getTempLocationF();
                 point[0] = x;
                 point[1] = y;
                 transformPointToViewLocal(point, child);
@@ -7354,6 +7379,97 @@
     }
 
     /**
+     * Offsets the given rectangle in parent's local coordinates into child's coordinate space
+     * and clips the result to the child View's bounds, padding and clipRect if appropriate. If the
+     * resulting rectangle is not empty, the request is forwarded to the child.
+     * <p>
+     * Note: This method does not account for any static View transformations which may be
+     * applied to the child view.
+     *
+     * @param child            the child to dispatch to
+     * @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates
+     * @param windowOffset     the offset of localVisibleRect within the window
+     * @param targets          a queue to collect located targets
+     */
+    private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect,
+            Point windowOffset, Queue<ScrollCaptureTarget> targets) {
+
+        // copy local visible rect for modification and dispatch
+        final Rect childVisibleRect = getTempRect();
+        childVisibleRect.set(localVisibleRect);
+
+        // transform to child coords
+        final Point childWindowOffset = getTempPoint();
+        childWindowOffset.set(windowOffset.x, windowOffset.y);
+
+        final int dx = child.mLeft - mScrollX;
+        final int dy = child.mTop - mScrollY;
+
+        childVisibleRect.offset(-dx, -dy);
+        childWindowOffset.offset(dx, dy);
+
+        boolean rectIsVisible = true;
+        final int width = mRight - mLeft;
+        final int height = mBottom - mTop;
+
+        // Clip to child bounds
+        if (getClipChildren()) {
+            rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight());
+        }
+
+        // Clip to child padding.
+        if (rectIsVisible && (child instanceof ViewGroup)
+                && ((ViewGroup) child).getClipToPadding()) {
+            rectIsVisible = childVisibleRect.intersect(
+                    child.mPaddingLeft, child.mPaddingTop,
+                    child.getWidth() - child.mPaddingRight,
+                    child.getHeight() - child.mPaddingBottom);
+        }
+        // Clip to child clipBounds.
+        if (rectIsVisible && child.mClipBounds != null) {
+            rectIsVisible = childVisibleRect.intersect(child.mClipBounds);
+        }
+        if (rectIsVisible) {
+            child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets);
+        }
+    }
+
+    /**
+     * Handle the scroll capture search request by checking this view if applicable, then to each
+     * child view.
+     *
+     * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to
+     *                         the parent
+     * @param windowOffset     the offset of this view within the window
+     * @param targets          the collected list of scroll capture targets
+     *
+     * @hide
+     */
+    @Override
+    public void dispatchScrollCaptureSearch(
+            @NonNull Rect localVisibleRect, @NonNull Point windowOffset,
+            @NonNull Queue<ScrollCaptureTarget> targets) {
+
+        // Dispatch to self first.
+        super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+
+        // Then dispatch to children, if not excluding descendants.
+        if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) == 0) {
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                // Only visible views can be captured.
+                if (child.getVisibility() != View.VISIBLE) {
+                    continue;
+                }
+                // Transform to child coords and dispatch
+                dispatchTransformedScrollCaptureSearch(child, localVisibleRect, windowOffset,
+                        targets);
+            }
+        }
+    }
+
+    /**
      * Returns the animation listener to which layout animation events are
      * sent.
      *
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ed1edc3..9d275cd 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -206,6 +206,7 @@
     private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV;
     private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV;
     private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV;
+    private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV;
 
     /**
      * Set to false if we do not want to use the multi threaded renderer even though
@@ -653,6 +654,8 @@
     private final InsetsController mInsetsController;
     private final ImeFocusController mImeFocusController;
 
+    private ScrollCaptureClient mScrollCaptureClient;
+
     /**
      * @return {@link ImeFocusController} for this instance.
      */
@@ -661,6 +664,11 @@
         return mImeFocusController;
     }
 
+    /** @return The current {@link ScrollCaptureClient} for this instance, if any is active. */
+    @Nullable
+    public ScrollCaptureClient getScrollCaptureClient() {
+        return mScrollCaptureClient;
+    }
 
     private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker();
 
@@ -694,6 +702,8 @@
     // draw returns.
     private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction();
 
+    private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks;
+
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
@@ -3769,7 +3779,9 @@
                 mNextReportConsumeBLAST = true;
                 mNextDrawUseBLASTSyncTransaction = false;
 
-                mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction);
+                if (mBlastBufferQueue != null) {
+                    mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction);
+                }
             }
             boolean canUseAsync = draw(fullRedrawNeeded);
             if (usingAsyncReport && !canUseAsync) {
@@ -4778,6 +4790,7 @@
     private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33;
     private static final int MSG_SHOW_INSETS = 34;
     private static final int MSG_HIDE_INSETS = 35;
+    private static final int MSG_REQUEST_SCROLL_CAPTURE = 36;
 
 
     final class ViewRootHandler extends Handler {
@@ -5080,6 +5093,9 @@
                 case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: {
                     updateLocationInParentDisplay(msg.arg1, msg.arg2);
                 } break;
+                case MSG_REQUEST_SCROLL_CAPTURE:
+                    handleScrollCaptureRequest((IScrollCaptureController) msg.obj);
+                    break;
             }
         }
     }
@@ -8789,6 +8805,131 @@
         return false;
     }
 
+    /**
+     * Adds a scroll capture callback to this window.
+     *
+     * @param callback the callback to add
+     */
+    public void addScrollCaptureCallback(ScrollCaptureCallback callback) {
+        if (mRootScrollCaptureCallbacks == null) {
+            mRootScrollCaptureCallbacks = new HashSet<>();
+        }
+        mRootScrollCaptureCallbacks.add(callback);
+    }
+
+    /**
+     * Removes a scroll capture callback from this window.
+     *
+     * @param callback the callback to remove
+     */
+    public void removeScrollCaptureCallback(ScrollCaptureCallback callback) {
+        if (mRootScrollCaptureCallbacks != null) {
+            mRootScrollCaptureCallbacks.remove(callback);
+            if (mRootScrollCaptureCallbacks.isEmpty()) {
+                mRootScrollCaptureCallbacks = null;
+            }
+        }
+    }
+
+    /**
+     * Dispatches a scroll capture request to the view hierarchy on the ui thread.
+     *
+     * @param controller the controller to receive replies
+     */
+    public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
+        mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, controller).sendToTarget();
+    }
+
+    /**
+     * Collect and include any ScrollCaptureCallback instances registered with the window.
+     *
+     * @see #addScrollCaptureCallback(ScrollCaptureCallback)
+     * @param targets the search queue for targets
+     */
+    private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) {
+        for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) {
+            // Add to the list for consideration
+            Point offset = new Point(mView.getLeft(), mView.getTop());
+            Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight());
+            targets.add(new ScrollCaptureTarget(mView, rect, offset, cb));
+        }
+    }
+
+    /**
+     * Handles an inbound request for scroll capture from the system. If a client is not already
+     * active, a search will be dispatched through the view tree to locate scrolling content.
+     * <p>
+     * Either {@link IScrollCaptureController#onClientConnected(IScrollCaptureClient, Rect,
+     * Point)} or {@link IScrollCaptureController#onClientUnavailable()} will be returned
+     * depending on the results of the search.
+     *
+     * @param controller the interface to the system controller
+     * @see ScrollCaptureTargetResolver
+     */
+    private void handleScrollCaptureRequest(@NonNull IScrollCaptureController controller) {
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Window (root) level callbacks
+        collectRootScrollCaptureTargets(targetList);
+
+        // Search through View-tree
+        View rootView = getView();
+        Point point = new Point();
+        Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight());
+        getChildVisibleRect(rootView, rect, point);
+        rootView.dispatchScrollCaptureSearch(rect, point, targetList);
+
+        // No-op path. Scroll capture not offered for this window.
+        if (targetList.isEmpty()) {
+            dispatchScrollCaptureSearchResult(controller, null);
+            return;
+        }
+
+        // Request scrollBounds from each of the targets.
+        // Continues with the consumer once all responses are consumed, or the timeout expires.
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetList);
+        resolver.start(mHandler, 1000,
+                (selected) -> dispatchScrollCaptureSearchResult(controller, selected));
+    }
+
+    /** Called by {@link #handleScrollCaptureRequest} when a result is returned */
+    private void dispatchScrollCaptureSearchResult(
+            @NonNull IScrollCaptureController controller,
+            @Nullable ScrollCaptureTarget selectedTarget) {
+
+        // If timeout or no eligible targets found.
+        if (selectedTarget == null) {
+            try {
+                if (DEBUG_SCROLL_CAPTURE) {
+                    Log.d(TAG, "scrollCaptureSearch returned no targets available.");
+                }
+                controller.onClientUnavailable();
+            } catch (RemoteException e) {
+                if (DEBUG_SCROLL_CAPTURE) {
+                    Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
+                }
+            }
+            return;
+        }
+
+        // Create a client instance and return it to the caller
+        mScrollCaptureClient = new ScrollCaptureClient(selectedTarget, controller);
+        try {
+            if (DEBUG_SCROLL_CAPTURE) {
+                Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureClient());
+            }
+            controller.onClientConnected(
+                    mScrollCaptureClient,
+                    selectedTarget.getScrollBounds(),
+                    selectedTarget.getPositionInWindow());
+        } catch (RemoteException e) {
+            if (DEBUG_SCROLL_CAPTURE) {
+                Log.w(TAG, "Failed to notify controller of scroll capture search result.", e);
+            }
+            mScrollCaptureClient.disconnect();
+            mScrollCaptureClient = null;
+        }
+    }
 
     private void reportNextDraw() {
         if (mReportNextDraw == false) {
@@ -9091,6 +9232,13 @@
             }
         }
 
+        @Override
+        public void requestScrollCapture(IScrollCaptureController controller) {
+            final ViewRootImpl viewAncestor = mViewAncestor.get();
+            if (viewAncestor != null) {
+                viewAncestor.dispatchScrollCaptureRequest(controller);
+            }
+        }
     }
 
     public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index ae9afaba..b153648 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -2535,6 +2535,33 @@
         return Collections.emptyList();
     }
 
+    /**
+     * System request to begin scroll capture.
+     *
+     * @param controller the controller to receive responses
+     * @hide
+     */
+    public void requestScrollCapture(IScrollCaptureController controller) {
+    }
+
+    /**
+     * Registers a {@link ScrollCaptureCallback} with the root of this window.
+     *
+     * @param callback the callback to add
+     * @hide
+     */
+    public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+    }
+
+    /**
+     * Unregisters a {@link ScrollCaptureCallback} previously registered with this window.
+     *
+     * @param callback the callback to remove
+     * @hide
+     */
+    public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+    }
+
     /** @hide */
     public void setTheme(int resId) {
     }
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index ec51301..397bce4 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -24,7 +24,6 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.MergedConfiguration;
-import android.view.IWindowSession;
 
 import java.util.HashMap;
 
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index d851a09..970bab9 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -2611,11 +2611,12 @@
      * does not match either the personal or work user handle.
      **/
     private int getProfileForUser(UserHandle currentUserHandle) {
-        if (currentUserHandle == getPersonalProfileUserHandle()) {
+        if (currentUserHandle.equals(getPersonalProfileUserHandle())) {
             return PROFILE_PERSONAL;
-        } else if (currentUserHandle == getWorkProfileUserHandle()) {
+        } else if (currentUserHandle.equals(getWorkProfileUserHandle())) {
             return PROFILE_WORK;
         }
+        Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile.");
         return -1;
     }
 
diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java
index 36eecfb..3eb0923 100644
--- a/core/java/com/android/internal/app/IntentForwarderActivity.java
+++ b/core/java/com/android/internal/app/IntentForwarderActivity.java
@@ -163,7 +163,7 @@
             return;
         }
         sanitizeIntent(innerIntent);
-        startActivity(intentReceived);
+        startActivityAsCaller(intentReceived, null, null, false, getUserId());
         finish();
     }
 
@@ -234,23 +234,7 @@
 
         Intent intentToCheck = forwardIntent;
         if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) {
-            // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded.
-            if (forwardIntent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) {
-                Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to"
-                        + " a different user");
-                return null;
-            }
-            if (forwardIntent.hasExtra(Intent.EXTRA_REPLACEMENT_EXTRAS)) {
-                Slog.wtf(TAG, "A chooser intent with replacement extras cannot be forwarded to a"
-                        + " different user");
-                return null;
-            }
-            intentToCheck = forwardIntent.getParcelableExtra(Intent.EXTRA_INTENT);
-            if (intentToCheck == null) {
-                Slog.wtf(TAG, "Cannot forward a chooser intent with no extra "
-                        + Intent.EXTRA_INTENT);
-                return null;
-            }
+            return null;
         }
         if (forwardIntent.getSelector() != null) {
             intentToCheck = forwardIntent.getSelector();
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 25c114f..23ba653 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -79,6 +79,7 @@
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.IRotationWatcher.Stub;
+import android.view.IScrollCaptureController;
 import android.view.IWindowManager;
 import android.view.InputDevice;
 import android.view.InputEvent;
@@ -89,6 +90,7 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
+import android.view.ScrollCaptureCallback;
 import android.view.SearchEvent;
 import android.view.SurfaceHolder.Callback2;
 import android.view.View;
@@ -3916,4 +3918,35 @@
                     : null);
         }
     }
+
+    /**
+     * System request to begin scroll capture.
+     *
+     * @param controller the controller to receive responses
+     * @hide
+     */
+    @Override
+    public void requestScrollCapture(IScrollCaptureController controller) {
+        getViewRootImpl().dispatchScrollCaptureRequest(controller);
+    }
+
+    /**
+     * Registers a handler providing scrolling capture support for window content.
+     *
+     * @param callback the callback to add
+     */
+    @Override
+    public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+        getViewRootImpl().addScrollCaptureCallback(callback);
+    }
+
+    /**
+     * Unregisters the given {@link ScrollCaptureCallback}.
+     *
+     * @param callback the callback to remove
+     */
+    @Override
+    public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) {
+        getViewRootImpl().removeScrollCaptureCallback(callback);
+    }
 }
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index 47f094f..7f3eb45 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -26,6 +26,7 @@
 import android.util.MergedConfiguration;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.IWindowSession;
 import android.view.InsetsSourceControl;
@@ -169,4 +170,13 @@
     @Override
     public void dispatchPointerCaptureChanged(boolean hasCapture) {
     }
+
+    @Override
+    public void requestScrollCapture(IScrollCaptureController controller) {
+        try {
+            controller.onClientUnavailable();
+        } catch (RemoteException ex) {
+            // ignore
+        }
+    }
 }
diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java
new file mode 100644
index 0000000..c589afde
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java
@@ -0,0 +1,117 @@
+/*
+ * 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.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.ScrollCaptureCallback;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Provides built-in framework level Scroll Capture support for standard scrolling Views.
+ */
+public class ScrollCaptureInternal {
+    private static final String TAG = "ScrollCaptureInternal";
+
+    private static final int UP = -1;
+    private static final int DOWN = 1;
+
+    /**
+     * Not a ViewGroup, or cannot scroll according to View APIs.
+     */
+    public static final int TYPE_FIXED = 0;
+
+    /**
+     * Slides a single child view using mScrollX/mScrollY.
+     */
+    public static final int TYPE_SCROLLING = 1;
+
+    /**
+     * Slides child views through the viewport by translating their layout positions with {@link
+     * View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and
+     * binding views to data from an adapter. Views are reused whenever possible.
+     */
+    public static final int TYPE_RECYCLING = 2;
+
+    /**
+     * Performs tests on the given View and determines:
+     * 1. If scrolling is possible
+     * 2. What mechanisms are used for scrolling.
+     * <p>
+     * This needs to be fast and not alloc memory. It's called on everything in the tree not marked
+     * as excluded during scroll capture search.
+     */
+    public static int detectScrollingType(View view) {
+        // Must be a ViewGroup
+        if (!(view instanceof ViewGroup)) {
+            return TYPE_FIXED;
+        }
+        // Confirm that it can scroll.
+        if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) {
+            // Nothing to scroll here, move along.
+            return TYPE_FIXED;
+        }
+        // ScrollViews accept only a single child.
+        if (((ViewGroup) view).getChildCount() > 1) {
+            return TYPE_RECYCLING;
+        }
+        //Because recycling containers don't use scrollY, a non-zero value means Scroll view.
+        if (view.getScrollY() != 0) {
+            return TYPE_SCROLLING;
+        }
+        // Since scrollY cannot be negative, this means a Recycling view.
+        if (view.canScrollVertically(UP)) {
+            return TYPE_RECYCLING;
+        }
+        // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1.
+
+        // For Recycling containers, this should be a no-op (RecyclerView logs a warning)
+        view.scrollTo(view.getScrollX(), 1);
+
+        // A scrolling container would have moved by 1px.
+        if (view.getScrollY() == 1) {
+            view.scrollTo(view.getScrollX(), 0);
+            return TYPE_SCROLLING;
+        }
+        return TYPE_RECYCLING;
+    }
+
+    /**
+     * Creates a scroll capture callback for the given view if possible.
+     *
+     * @param view             the view to capture
+     * @param localVisibleRect the visible area of the given view in local coordinates, as supplied
+     *                         by the view parent
+     * @param positionInWindow the offset of localVisibleRect within the window
+     *
+     * @return a new callback or null if the View isn't supported
+     */
+    @Nullable
+    public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect,
+            Point positionInWindow) {
+        // Nothing to see here yet.
+        int i = detectScrollingType(view);
+        switch (i) {
+            case TYPE_SCROLLING:
+                return new ScrollCaptureViewSupport<>((ViewGroup) view,
+                        new ScrollViewCaptureHelper());
+        }
+        return null;
+    }
+}
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
new file mode 100644
index 0000000..9f100bd
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.view.View;
+
+interface ScrollCaptureViewHelper<V extends View> {
+    int UP = -1;
+    int DOWN = 1;
+
+    /**
+     * Verifies that the view is still visible and scrollable. If true is returned here, expect a
+     * call to {@link #onComputeScrollBounds(View)} to follow.
+     *
+     * @param view the view being captured
+     * @return true if the callback should respond to a request with scroll bounds
+     */
+    default boolean onAcceptSession(@Nullable V view) {
+        return view != null && view.isVisibleToUser()
+                && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN));
+    }
+
+    /**
+     * Given a scroll capture request for a view, adjust the provided rect to cover the scrollable
+     * content area. The default implementation returns the padded content area of {@code view}.
+     *
+     * @param view the view being captured
+     */
+    default Rect onComputeScrollBounds(@Nullable V view) {
+        return new Rect(view.getPaddingLeft(), view.getPaddingTop(),
+                view.getWidth() - view.getPaddingRight(),
+                view.getHeight() - view.getPaddingBottom());
+    }
+    /**
+     * Adjust the target for capture.
+     * <p>
+     * Do not touch anything that may change layout positions or sizes on screen. Anything else may
+     * be adjusted as long as it can be reversed in {@link #onPrepareForEnd(View)}.
+     *
+     * @param view         the view being captured
+     * @param scrollBounds the bounds within {@code view} where content scrolls
+     */
+    void onPrepareForStart(@NonNull V view, Rect scrollBounds);
+
+    /**
+     * Map the request onto the screen.
+     * <p>
+     * Given a  rect describing the area to capture, relative to scrollBounds, take actions
+     * necessary to bring the content within the rectangle into the visible area of the view if
+     * needed and return the resulting rectangle describing the position and bounds of the area
+     * which is visible.
+     *
+     * @param scrollBounds the area in which scrolling content moves, local to the {@code containing
+     *                     view}
+     * @param requestRect  the area relative to {@code scrollBounds} which describes the location of
+     *                     content to capture for the request
+     * @return the visible area within scrollBounds of the requested rectangle, return {@code null}
+     * in the case of an unrecoverable error condition, to abort the capture process
+     */
+    Rect onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect);
+
+    /**
+     * Restore the target after capture.
+     * <p>
+     * Put back anything that was changed in {@link #onPrepareForStart(View, Rect)}.
+     *
+     * @param view the view being captured
+     */
+    void onPrepareForEnd(@NonNull V view);
+}
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
new file mode 100644
index 0000000..4087eda
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
@@ -0,0 +1,239 @@
+/*
+ * 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.graphics.HardwareRenderer;
+import android.graphics.Matrix;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.RenderNode;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.view.ScrollCaptureCallback;
+import android.view.ScrollCaptureSession;
+import android.view.Surface;
+import android.view.View;
+
+import java.lang.ref.WeakReference;
+import java.util.function.Consumer;
+
+/**
+ * Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling
+ * containers.
+ * <p>
+ * To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper}
+ * to the callback.
+ *
+ * @param <V> the specific View subclass handled
+ * @hide
+ */
+public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback {
+
+    private final WeakReference<V> mWeakView;
+    private final ScrollCaptureViewHelper<V> mViewHelper;
+    private ViewRenderer mRenderer;
+    private Handler mUiHandler;
+    private boolean mStarted;
+    private boolean mEnded;
+
+    static <V extends View> ScrollCaptureCallback createCallback(V view,
+            ScrollCaptureViewHelper<V> impl) {
+        return new ScrollCaptureViewSupport<>(view, impl);
+    }
+
+    ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) {
+        mWeakView = new WeakReference<>(containingView);
+        mRenderer = new ViewRenderer();
+        mUiHandler = containingView.getHandler();
+        mViewHelper = viewHelper;
+    }
+
+    // Base implementation of ScrollCaptureCallback
+
+    @Override
+    public final void onScrollCaptureSearch(Consumer<Rect> onReady) {
+        V view = mWeakView.get();
+        mStarted = false;
+        mEnded = false;
+
+        if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) {
+            onReady.accept(mViewHelper.onComputeScrollBounds(view));
+            return;
+        }
+        onReady.accept(null);
+    }
+
+    @Override
+    public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
+        V view = mWeakView.get();
+        mEnded = false;
+        mStarted = true;
+
+        // Note: If somehow the view is already gone or detached, the first call to
+        // {@code onScrollCaptureImageRequest} will return an error and request the session to
+        // end.
+        if (view != null && view.isVisibleToUser()) {
+            mRenderer.setSurface(session.getSurface());
+            mViewHelper.onPrepareForStart(view, session.getScrollBounds());
+        }
+        onReady.run();
+    }
+
+    @Override
+    public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) {
+        V view = mWeakView.get();
+        if (view == null || !view.isVisibleToUser()) {
+            // Signal to the controller that we have a problem and can't continue.
+            session.notifyBufferSent(0, null);
+            return;
+        }
+        Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(),
+                requestRect);
+        mRenderer.renderFrame(view, captureArea, mUiHandler,
+                () -> session.notifyBufferSent(0, captureArea));
+    }
+
+    @Override
+    public final void onScrollCaptureEnd(Runnable onReady) {
+        V view = mWeakView.get();
+        if (mStarted && !mEnded) {
+            mViewHelper.onPrepareForEnd(view);
+            /* empty */
+            mEnded = true;
+            mRenderer.trimMemory();
+            mRenderer.setSurface(null);
+        }
+        onReady.run();
+    }
+
+    /**
+     * Internal helper class which assists in rendering sections of the view hierarchy relative to a
+     * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch
+     * image requests.
+     */
+    static final class ViewRenderer {
+        // alpha, "reasonable default" from Javadoc
+        private static final float AMBIENT_SHADOW_ALPHA = 0.039f;
+        private static final float SPOT_SHADOW_ALPHA = 0.039f;
+
+        // Default values:
+        //    lightX = (screen.width() / 2) - windowLeft
+        //    lightY = 0 - windowTop
+        //    lightZ = 600dp
+        //    lightRadius = 800dp
+        private static final float LIGHT_Z_DP = 400;
+        private static final float LIGHT_RADIUS_DP = 800;
+        private static final String TAG = "ViewRenderer";
+
+        private HardwareRenderer mRenderer;
+        private RenderNode mRootRenderNode;
+        private final RectF mTempRectF = new RectF();
+        private final Rect mSourceRect = new Rect();
+        private final Rect mTempRect = new Rect();
+        private final Matrix mTempMatrix = new Matrix();
+        private final int[] mTempLocation = new int[2];
+        private long mLastRenderedSourceDrawingId = -1;
+
+
+        ViewRenderer() {
+            mRenderer = new HardwareRenderer();
+            mRootRenderNode = new RenderNode("ScrollCaptureRoot");
+            mRenderer.setContentRoot(mRootRenderNode);
+
+            // TODO: Figure out a way to flip this on when we are sure the source window is opaque
+            mRenderer.setOpaque(false);
+        }
+
+        public void setSurface(Surface surface) {
+            mRenderer.setSurface(surface);
+        }
+
+        /**
+         * Cache invalidation check. If the source view is the same as the previous call (which is
+         * mostly always the case, then we can skip setting up lighting on each call (for now)
+         *
+         * @return true if the view changed, false if the view was previously rendered by this class
+         */
+        private boolean updateForView(View source) {
+            if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) {
+                return false;
+            }
+            mLastRenderedSourceDrawingId = source.getUniqueDrawingId();
+            return true;
+        }
+
+        // TODO: may need to adjust lightY based on the virtual canvas position to get
+        //       consistent shadow positions across the whole capture. Or possibly just
+        //       pull lightZ way back to make shadows more uniform.
+        private void setupLighting(View mSource) {
+            mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId();
+            DisplayMetrics metrics = mSource.getResources().getDisplayMetrics();
+            mSource.getLocationOnScreen(mTempLocation);
+            final float lightX = metrics.widthPixels / 2f - mTempLocation[0];
+            final float lightY = metrics.heightPixels - mTempLocation[1];
+            final int lightZ = (int) (LIGHT_Z_DP * metrics.density);
+            final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density);
+
+            // Enable shadows for elevation/Z
+            mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius);
+            mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA);
+
+        }
+
+        public void renderFrame(View localReference, Rect sourceRect, Handler handler,
+                Runnable onFrameCommitted) {
+            if (updateForView(localReference)) {
+                setupLighting(localReference);
+            }
+            buildRootDisplayList(localReference, sourceRect);
+            HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
+            request.setVsyncTime(SystemClock.elapsedRealtimeNanos());
+            request.setFrameCommitCallback(handler::post, onFrameCommitted);
+            request.setWaitForPresent(true);
+            request.syncAndDraw();
+        }
+
+        public void trimMemory() {
+            mRenderer.clearContent();
+        }
+
+        public void destroy() {
+            mRenderer.destroy();
+        }
+
+        private void transformToRoot(View local, Rect localRect, Rect outRect) {
+            mTempMatrix.reset();
+            local.transformMatrixToGlobal(mTempMatrix);
+            mTempRectF.set(localRect);
+            mTempMatrix.mapRect(mTempRectF);
+            mTempRectF.round(outRect);
+        }
+
+        private void buildRootDisplayList(View source, Rect localSourceRect) {
+            final View captureSource = source.getRootView();
+            transformToRoot(source, localSourceRect, mTempRect);
+            mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height());
+            RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(),
+                    mTempRect.height());
+            canvas.translate(-mTempRect.left, -mTempRect.top);
+            canvas.drawRenderNode(captureSource.updateDisplayListIfDirty());
+            mRootRenderNode.endRecording();
+        }
+    }
+}
diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
new file mode 100644
index 0000000..12bd461
--- /dev/null
+++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java
@@ -0,0 +1,167 @@
+/*
+ * 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.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+/**
+ * ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups.
+ * <p>
+ * Requirements for proper operation:
+ * <ul>
+ * <li>contains at most 1 child.
+ * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}.
+ * <li>has a finite, known content height and scrolling range
+ * <li>correctly implements {@link View#canScrollVertically(int)}
+ * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View,
+ * Rect, boolean)}
+ * </ul>
+ */
+public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> {
+    private int mStartScrollY;
+    private boolean mScrollBarEnabled;
+    private int mOverScrollMode;
+
+    /** @see ScrollCaptureViewHelper#onPrepareForStart(View, Rect) */
+    public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) {
+        mStartScrollY = view.getScrollY();
+        mOverScrollMode = view.getOverScrollMode();
+        if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
+            view.setOverScrollMode(View.OVER_SCROLL_NEVER);
+        }
+        mScrollBarEnabled = view.isVerticalScrollBarEnabled();
+        if (mScrollBarEnabled) {
+            view.setVerticalScrollBarEnabled(false);
+        }
+    }
+
+    /** @see ScrollCaptureViewHelper#onScrollRequested(View, Rect, Rect) */
+    public Rect onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) {
+        final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE
+        if (contentView == null) {
+            return null;
+        }
+        /*
+               +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000)
+               |         |
+            ...|.........|...  startScrollY=100
+               |         |
+            +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200)
+            |  .         .   |
+        --- |  . +-----+   <------+ Scroll Bounds [50,50 - 250,250] (200x200)
+         ^  |  . |     | .   |      (Local to Container View, fixed/un-scrolled)
+         |  |  . |     | .   |
+         |  |  . |     | .   |
+         |  |  . +-----+ .   |
+         |  |  .         .   |
+         |  +--+---------+---+
+         |     |         |
+        -+-    | +-----+ |
+               | |#####| |   <--+ Requested Bounds [0,300 - 200,400] (200x100)
+               | +-----+ |       (Local to Scroll Bounds, fixed/un-scrolled)
+               |         |
+               +---------+
+
+        Container View (ScrollView) [0,0 - 300,300] (scrollY = 200)
+        \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
+        \__ Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
+            \__ Requested Bounds[0,300 - 200,400] (200x100)
+       */
+
+        // 0) adjust the requestRect to account for scroll change since start
+        //
+        //  Scroll Bounds[50,50 - 250,250]  (w=200,h=200)
+        //  \__ Requested Bounds[0,200 - 200,300] (200x100)
+
+        // (y-100) (scrollY - mStartScrollY)
+        int scrollDelta = view.getScrollY() - mStartScrollY;
+
+        //  1) Translate request rect to make it relative to container view
+        //
+        //  Container View [0,0 - 300,300] (scrollY=200)
+        //  \__ Requested Bounds[50,250 - 250,350] (w=250, h=100)
+
+        // (x+50,y+50)
+        Rect requestedContainerBounds = new Rect(requestRect);
+        requestedContainerBounds.offset(0, -scrollDelta);
+        requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top);
+
+        //  2) Translate from container to contentView relative (applying container scrollY)
+        //
+        //  Container View [0,0 - 300,300] (scrollY=200)
+        //  \__ Content [25,25 - 275,1025]  (250x1000) (contentView)
+        //      \__ Requested Bounds[25,425 - 200,525] (w=250, h=100)
+
+        // (x-25,y+175) (scrollY - content.top)
+        Rect requestedContentBounds = new Rect(requestedContainerBounds);
+        requestedContentBounds.offset(
+                view.getScrollX() - contentView.getLeft(),
+                view.getScrollY() - contentView.getTop());
+
+
+
+        // requestRect is now local to contentView as requestedContentBounds
+        // contentView (and each parent in turn if possible) will be scrolled
+        // (if necessary) to make all of requestedContent visible, (if possible!)
+        contentView.requestRectangleOnScreen(new Rect(requestedContentBounds), true);
+
+        // update new offset between starting and current scroll position
+        scrollDelta = view.getScrollY() - mStartScrollY;
+
+
+        // TODO: adjust to avoid occlusions/minimize scroll changes
+
+        Point offset = new Point();
+        final Rect capturedRect = new Rect(requestedContentBounds); // empty
+        if (!view.getChildVisibleRect(contentView, capturedRect, offset)) {
+            capturedRect.setEmpty();
+            return capturedRect;
+        }
+        // Transform back from global to content-view local
+        capturedRect.offset(-offset.x, -offset.y);
+
+        // Then back to container view
+        capturedRect.offset(
+                contentView.getLeft() - view.getScrollX(),
+                contentView.getTop() - view.getScrollY());
+
+
+        // And back to relative to scrollBounds
+        capturedRect.offset(-scrollBounds.left, -scrollBounds.top);
+
+        // Apply scrollDelta again to return to make capturedRect relative to scrollBounds at
+        // the scroll position at start of capture.
+        capturedRect.offset(0, scrollDelta);
+        return capturedRect;
+    }
+
+    /** @see ScrollCaptureViewHelper#onPrepareForEnd(View)  */
+    public void onPrepareForEnd(@NonNull ViewGroup view) {
+        view.scrollTo(0, mStartScrollY);
+        if (mOverScrollMode != View.OVER_SCROLL_NEVER) {
+            view.setOverScrollMode(mOverScrollMode);
+        }
+        if (mScrollBarEnabled) {
+            view.setVerticalScrollBarEnabled(true);
+        }
+    }
+}
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index ba7aef7..b51d4f5 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -1244,12 +1244,11 @@
 {
     if (mExitWithoutCleanup) {
         ALOGI("VM exiting with result code %d, cleanup skipped.", code);
-        ::_exit(code);
     } else {
         ALOGI("VM exiting with result code %d.", code);
         onExit(code);
-        ::exit(code);
     }
+    ::_exit(code);
 }
 
 void AndroidRuntime::onVmCreated(JNIEnv* env)
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 924dc4b..21985f0 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -119,6 +119,7 @@
 
 static pid_t gSystemServerPid = 0;
 
+static constexpr const char* kVoldAppDataIsolation = "persist.sys.vold_app_data_isolation_enabled";
 static constexpr const char* kPropFuse = "persist.sys.fuse";
 static const char kZygoteClassName[] = "com/android/internal/os/Zygote";
 static jclass gZygoteClass;
@@ -831,6 +832,7 @@
              multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn);
 
   bool isFuse = GetBoolProperty(kPropFuse, false);
+  bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false);
 
   if (isFuse) {
     if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) {
@@ -840,6 +842,9 @@
     } else if (mount_mode == MOUNT_EXTERNAL_INSTALLER) {
       const std::string installer_source = StringPrintf("/mnt/installer/%d", user_id);
       BindMount(installer_source, "/storage", fail_fn);
+    } else if (isAppDataIsolationEnabled && mount_mode == MOUNT_EXTERNAL_ANDROID_WRITABLE) {
+      const std::string writable_source = StringPrintf("/mnt/androidwritable/%d", user_id);
+      BindMount(writable_source, "/storage", fail_fn);
     } else {
       BindMount(user_source, "/storage", fail_fn);
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6e8b9de..ee25ac2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1111,12 +1111,13 @@
          grants your app this permission. If you don't need this permission, be sure your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
          targetSdkVersion}</a> is 4 or higher.
-         <p>Protection level: normal
+         <p>Protection level: dangerous
     -->
     <permission android:name="android.permission.READ_PHONE_STATE"
+        android:permissionGroup="android.permission-group.UNDEFINED"
         android:label="@string/permlab_readPhoneState"
         android:description="@string/permdesc_readPhoneState"
-        android:protectionLevel="normal" />
+        android:protectionLevel="dangerous" />
 
     <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities
          granted by {@link #READ_PHONE_STATE} but is exposed to instant applications.
diff --git a/core/res/res/layout/resolver_empty_states.xml b/core/res/res/layout/resolver_empty_states.xml
index 5890bed..25615d2 100644
--- a/core/res/res/layout/resolver_empty_states.xml
+++ b/core/res/res/layout/resolver_empty_states.xml
@@ -43,6 +43,7 @@
             android:fontFamily="@string/config_headlineFontFamilyMedium"
             android:textColor="@color/resolver_empty_state_text"
             android:textSize="14sp"
+            android:gravity="center_horizontal"
             android:layout_centerHorizontal="true" />
         <TextView
             android:id="@+id/resolver_empty_state_subtitle"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 4065a6c..a8d1605 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2527,6 +2527,21 @@
             <flag name="noExcludeDescendants" value="0x8" />
         </attr>
 
+        <!-- Hints the Android System whether the this View should be considered a scroll capture target. -->
+        <attr name="scrollCaptureHint">
+            <!-- Let the Android System  determine if the view can be a scroll capture target. -->
+            <flag name="auto" value="0" />
+            <!-- Hint the Android System that this view is a likely target. If capable, it will
+                 be ranked above other views without this flag. -->
+            <flag name="include" value="0x1" />
+            <!-- Hint the Android System that this view should never be considered a scroll capture
+                 target. -->
+            <flag name="exclude" value="0x2" />
+            <!-- Hint the Android System that this view's children should not be examined and should
+                 be excluded as a scroll capture target. -->
+            <flag name="excludeDescendants" value="0x4" />
+        </attr>
+
         <!-- Boolean that controls whether a view can take focus while in touch mode.
              If this is true for a view, that view can gain focus when clicked on, and can keep
              focus if another view is clicked on that doesn't have this attribute set to true. -->
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 67d20da..fb887c3 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3020,6 +3020,8 @@
       <public name="preserveLegacyExternalStorage" />
       <public name="mimeGroup" />
       <public name="gwpAsanMode" />
+      <!-- @hide -->
+      <public name="scrollCaptureHint" />
     </public-group>
 
     <public-group type="drawable" first-id="0x010800b5">
diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml
index 9110661..873b9eb 100644
--- a/core/res/res/xml/default_zen_mode_config.xml
+++ b/core/res/res/xml/default_zen_mode_config.xml
@@ -20,8 +20,8 @@
 <!-- Default configuration for zen mode.  See android.service.notification.ZenModeConfig. -->
 <zen version="9">
     <allow alarms="true" media="true" system="false" calls="true" callsFrom="2" messages="false"
-            reminders="false" events="false" repeatCallers="true" conversations="true"
-            conversationsFrom="2"/>
+            reminders="false" events="false" repeatCallers="true" convos="false"
+            convosFrom="3"/>
     <automatic ruleId="EVENTS_DEFAULT_RULE" enabled="false" snoozing="false" name="Event" zen="1"
                component="android/com.android.server.notification.EventConditionProvider"
                conditionId="condition://android/event?userId=-10000&amp;calendar=&amp;reply=1"/>
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index ea778fd..a354f1d 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -22,9 +22,12 @@
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.content.ContentInterface;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.net.Uri;
@@ -37,6 +40,8 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class VibrationEffectTest {
+    private static final float SCALE_TOLERANCE = 1e-2f;
+
     private static final String RINGTONE_URI_1 = "content://test/system/ringtone_1";
     private static final String RINGTONE_URI_2 = "content://test/system/ringtone_2";
     private static final String RINGTONE_URI_3 = "content://test/system/ringtone_3";
@@ -54,6 +59,12 @@
             VibrationEffect.createOneShot(TEST_TIMING, VibrationEffect.DEFAULT_AMPLITUDE);
     private static final VibrationEffect TEST_WAVEFORM =
             VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1);
+    private static final VibrationEffect TEST_COMPOSED =
+            VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10)
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 100)
+                    .compose();
 
     @Test
     public void getRingtones_noPrebakedRingtones() {
@@ -123,8 +134,14 @@
 
     @Test
     public void testScaleWaveform() {
-        VibrationEffect.Waveform scaled =
-                ((VibrationEffect.Waveform) TEST_WAVEFORM).scale(1.1f, 200);
+        VibrationEffect.Waveform initial = (VibrationEffect.Waveform) TEST_WAVEFORM;
+
+        VibrationEffect.Waveform copied = initial.scale(1f, 255);
+        assertEquals(255, copied.getAmplitudes()[0]);
+        assertEquals(0, copied.getAmplitudes()[1]);
+        assertEquals(-1, copied.getAmplitudes()[2]);
+
+        VibrationEffect.Waveform scaled = initial.scale(1.1f, 200);
         assertEquals(200, scaled.getAmplitudes()[0]);
         assertEquals(0, scaled.getAmplitudes()[1]);
     }
@@ -156,6 +173,66 @@
         }
     }
 
+    @Test
+    public void testScaleComposed() {
+        VibrationEffect.Composed initial = (VibrationEffect.Composed) TEST_COMPOSED;
+
+        VibrationEffect.Composed copied = initial.scale(1, 255);
+        assertEquals(1f, copied.getPrimitiveEffects().get(0).scale);
+        assertEquals(0.5f, copied.getPrimitiveEffects().get(1).scale);
+        assertEquals(0f, copied.getPrimitiveEffects().get(2).scale);
+
+        VibrationEffect.Composed halved = initial.scale(1, 128);
+        assertEquals(0.5f, halved.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE);
+        assertEquals(0.25f, halved.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE);
+        assertEquals(0f, halved.getPrimitiveEffects().get(2).scale);
+
+        VibrationEffect.Composed scaledUp = initial.scale(0.5f, 255);
+        assertEquals(1f, scaledUp.getPrimitiveEffects().get(0).scale); // does not scale up from 1
+        assertTrue(0.5f < scaledUp.getPrimitiveEffects().get(1).scale);
+        assertEquals(0f, scaledUp.getPrimitiveEffects().get(2).scale);
+
+        VibrationEffect.Composed restored = scaledUp.scale(2, 255);
+        assertEquals(1f, restored.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE);
+        assertEquals(0.5f, restored.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE);
+        assertEquals(0f, restored.getPrimitiveEffects().get(2).scale);
+
+        VibrationEffect.Composed scaledDown = initial.scale(2, 255);
+        assertEquals(1f, scaledDown.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE);
+        assertTrue(0.5f > scaledDown.getPrimitiveEffects().get(1).scale);
+        assertEquals(0f, scaledDown.getPrimitiveEffects().get(2).scale, SCALE_TOLERANCE);
+
+        VibrationEffect.Composed changeMax = initial.scale(1f, 51);
+        assertEquals(0.2f, changeMax.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE);
+        assertEquals(0.1f, changeMax.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE);
+        assertEquals(0f, changeMax.getPrimitiveEffects().get(2).scale);
+    }
+
+    @Test
+    public void testScaleComposedFailsWhenMaxAmplitudeAboveThreshold() {
+        try {
+            ((VibrationEffect.Composed) TEST_COMPOSED).scale(1.1f, 1000);
+            fail("Max amplitude above threshold, should throw IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testScaleAppliesSameAdjustmentsOnAllEffects() {
+        VibrationEffect.OneShot oneShot = new VibrationEffect.OneShot(TEST_TIMING, TEST_AMPLITUDE);
+        VibrationEffect.Waveform waveform = new VibrationEffect.Waveform(
+                new long[] { TEST_TIMING }, new int[]{ TEST_AMPLITUDE }, -1);
+        VibrationEffect.Composed composed =
+                (VibrationEffect.Composed) VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, TEST_AMPLITUDE / 255f)
+                    .compose();
+
+        assertEquals(oneShot.scale(2f, 128).getAmplitude(),
+                waveform.scale(2f, 128).getAmplitudes()[0]);
+        assertEquals(oneShot.scale(2f, 128).getAmplitude() / 255f, // convert amplitude to scale
+                composed.scale(2f, 128).getPrimitiveEffects().get(0).scale,
+                SCALE_TOLERANCE);
+    }
 
     private Resources mockRingtoneResources() {
         return mockRingtoneResources(new String[] {
@@ -172,9 +249,22 @@
         return mockResources;
     }
 
-    private Context mockContext(Resources r) {
-        Context ctx = mock(Context.class);
-        when(ctx.getResources()).thenReturn(r);
-        return ctx;
+    private Context mockContext(Resources resources) {
+        Context context = mock(Context.class);
+        ContentInterface contentInterface = mock(ContentInterface.class);
+        ContentResolver contentResolver = ContentResolver.wrap(contentInterface);
+
+        try {
+            // ContentResolver#uncanonicalize is final, so we need to mock the ContentInterface it
+            // delegates the call to for the tests that require matching with the mocked URIs.
+            when(contentInterface.uncanonicalize(any())).then(
+                    invocation -> invocation.getArgument(0));
+            when(context.getContentResolver()).thenReturn(contentResolver);
+            when(context.getResources()).thenReturn(resources);
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+
+        return context;
     }
 }
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java
new file mode 100644
index 0000000..e6ac2d6
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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 android.view;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Tests of {@link ScrollCaptureClient}.
+ */
+@SuppressWarnings("UnnecessaryLocalVariable")
+@RunWith(AndroidJUnit4.class)
+public class ScrollCaptureClientTest {
+
+    private final Point mPositionInWindow = new Point(1, 2);
+    private final Rect mLocalVisibleRect = new Rect(2, 3, 4, 5);
+    private final Rect mScrollBounds = new Rect(3, 4, 5, 6);
+
+    private Handler mHandler;
+    private ScrollCaptureTarget mTarget1;
+
+    @Mock
+    private Surface mSurface;
+    @Mock
+    private IScrollCaptureController mClientCallbacks;
+    @Mock
+    private View mMockView1;
+    @Mock
+    private ScrollCaptureCallback mCallback1;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mHandler = new Handler(getTargetContext().getMainLooper());
+
+        when(mMockView1.getHandler()).thenReturn(mHandler);
+        when(mMockView1.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        mTarget1 = new ScrollCaptureTarget(
+                mMockView1, mLocalVisibleRect, mPositionInWindow, mCallback1);
+        mTarget1.setScrollBounds(mScrollBounds);
+    }
+
+    /** Test the DelayedAction timeout helper class works as expected. */
+    @Test
+    public void testDelayedAction() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(200);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        getInstrumentation().waitForIdleSync();
+        assertFalse(delayed.cancel());
+        assertFalse(delayed.timeoutNow());
+        verify(action, times(1)).run();
+    }
+
+    /** Test the DelayedAction cancel() */
+    @Test
+    public void testDelayedAction_cancel() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(50);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        assertTrue(delayed.cancel());
+        assertFalse(delayed.timeoutNow());
+        try {
+            Thread.sleep(200);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        getInstrumentation().waitForIdleSync();
+        verify(action, never()).run();
+    }
+
+    /** Test the DelayedAction timeoutNow() - for testing only */
+    @Test
+    public void testDelayedAction_timeoutNow() {
+        Runnable action = Mockito.mock(Runnable.class);
+        ScrollCaptureClient.DelayedAction delayed =
+                new ScrollCaptureClient.DelayedAction(mHandler, 100, action);
+        try {
+            Thread.sleep(50);
+        } catch (InterruptedException ex) {
+            /* ignore */
+        }
+        assertTrue(delayed.timeoutNow());
+        assertFalse(delayed.cancel());
+        getInstrumentation().waitForIdleSync();
+        verify(action, times(1)).run();
+    }
+
+    /** Test creating a client with valid info */
+    @Test
+    public void testConstruction() {
+        new ScrollCaptureClient(mTarget1, mClientCallbacks);
+    }
+
+    /** Test creating a client fails if arguments are not valid. */
+    @Test
+    public void testConstruction_requiresScrollBounds() {
+        try {
+            mTarget1.setScrollBounds(null);
+            new ScrollCaptureClient(mTarget1, mClientCallbacks);
+            fail("An exception was expected.");
+        } catch (RuntimeException ex) {
+            // Ignore, expected.
+        }
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private static Answer<Void> runRunnable(int arg) {
+        return invocation -> {
+            Runnable r = invocation.getArgument(arg);
+            r.run();
+            return null;
+        };
+    }
+
+    @SuppressWarnings("SameParameterValue")
+    private static Answer<Void> reportBufferSent(int sessionArg, long frameNum, Rect capturedArea) {
+        return invocation -> {
+            ScrollCaptureSession session = invocation.getArgument(sessionArg);
+            session.notifyBufferSent(frameNum, capturedArea);
+            return null;
+        };
+    }
+
+    /** @see ScrollCaptureClient#startCapture(Surface) */
+    @Test
+    public void testStartCapture() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+
+        // Have the session start accepted immediately
+        doAnswer(runRunnable(1)).when(mCallback1)
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        client.startCapture(mSurface);
+        getInstrumentation().waitForIdleSync();
+
+        verify(mCallback1, times(1))
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        verify(mClientCallbacks, times(1)).onCaptureStarted();
+        verifyNoMoreInteractions(mClientCallbacks);
+    }
+
+    @Test
+    public void testStartCaptureTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        client.startCapture(mSurface);
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+    }
+
+    private void startClient(ScrollCaptureClient client) throws Exception {
+        doAnswer(runRunnable(1)).when(mCallback1)
+                .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class));
+        client.startCapture(mSurface);
+        getInstrumentation().waitForIdleSync();
+        reset(mCallback1, mClientCallbacks);
+    }
+
+    /** @see ScrollCaptureClient#requestImage(Rect) */
+    @Test
+    public void testRequestImage() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Stub the callback to complete the request immediately
+        doAnswer(reportBufferSent(/* sessionArg */ 0, /* frameNum */ 1L, new Rect(1, 2, 3, 4)))
+                .when(mCallback1)
+                .onScrollCaptureImageRequest(any(ScrollCaptureSession.class), any(Rect.class));
+
+        // Make the inbound binder call
+        client.requestImage(new Rect(1, 2, 3, 4));
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureImageRequest(
+                any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onCaptureBufferSent(eq(1L), eq(new Rect(1, 2, 3, 4)));
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    @Test
+    public void testRequestImageTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Make the inbound binder call
+        client.requestImage(new Rect(1, 2, 3, 4));
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureImageRequest(
+                any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4)));
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+        getInstrumentation().waitForIdleSync();
+
+        // (callback not stubbed, does nothing)
+        // Timeout triggers request to end capture
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    /** @see ScrollCaptureClient#endCapture() */
+    @Test
+    public void testEndCapture() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Stub the callback to complete the request immediately
+        doAnswer(runRunnable(0))
+                .when(mCallback1)
+                .onScrollCaptureEnd(any(Runnable.class));
+
+        // Make the inbound binder call
+        client.endCapture();
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onConnectionClosed();
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+
+    @Test
+    public void testEndCaptureTimeout() throws Exception {
+        final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks);
+        startClient(client);
+
+        // Make the inbound binder call
+        client.endCapture();
+
+        // Wait for handler thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class));
+
+        // Force timeout to fire
+        client.getTimeoutAction().timeoutNow();
+
+        // Wait for binder thread dispatch
+        getInstrumentation().waitForIdleSync();
+        verify(mClientCallbacks, times(1)).onConnectionClosed();
+
+        verifyNoMoreInteractions(mCallback1, mClientCallbacks);
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java
new file mode 100644
index 0000000..8b21b8e
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java
@@ -0,0 +1,498 @@
+/*
+ * 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 android.view;
+
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.LinkedList;
+import java.util.function.Consumer;
+
+/**
+ * Tests of {@link ScrollCaptureTargetResolver}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ScrollCaptureTargetResolverTest {
+
+    private static final long TEST_TIMEOUT_MS = 2000;
+    private static final long RESOLVER_TIMEOUT_MS = 1000;
+
+    private Handler mHandler;
+    private TargetConsumer mTargetConsumer;
+
+    @Before
+    public void setUp() {
+        mTargetConsumer = new TargetConsumer();
+        mHandler = new Handler(getTargetContext().getMainLooper());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testEmptyQueue() throws InterruptedException {
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(new LinkedList<>());
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertNull("Expected null due to empty queue", result);
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testNoValidTargets() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        // Supplies scrollBounds = null
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(null);
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Supplies scrollBounds = empty rect
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect());
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertNull("Expected null due to no valid targets", result);
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testSingleTarget() throws InterruptedException {
+        FakeScrollCaptureCallback callback = new FakeScrollCaptureCallback();
+        ScrollCaptureTarget target = createTarget(callback,
+                new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        callback.setScrollBounds(new Rect(2, 2, 18, 18));
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target);
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Excepted the same target as a result", target, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(2, 2, 18, 18), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testSingleTarget_backgroundThread() throws InterruptedException {
+        BackgroundTestCallback callback1 = new BackgroundTestCallback();
+        ScrollCaptureTarget target1 = createTarget(callback1,
+                new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        callback1.setDelay(100);
+        callback1.setScrollBounds(new Rect(2, 2, 18, 18));
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target1);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Excepted the single target1 as a result", target1, result);
+        assertEquals("Result has wrong scroll bounds",
+                new Rect(2, 2, 18, 18), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testPreferNonEmptyBounds() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect());
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(null);
+        ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
+                new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2); // scrollBounds not null or empty()
+        targetQueue.add(target3);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertEquals("Expected " + target2 + " as a result", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 20, 20), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testPreferHintInclude() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(1, 1, 19, 19));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE);
+
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(new Rect(2, 2, 18, 18));
+        ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50),
+                new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1);
+        targetQueue.add(target2); // * INCLUDE > AUTO
+        targetQueue.add(target3);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertEquals("input = " + targetQueue + " Expected " + target2
+                + " as the result, due to hint=INCLUDE", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(1, 1, 19, 19), result.getScrollBounds());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testDescendantPreferred() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        ViewGroup targetView1 = new FakeRootView(getTargetContext(), 0, 0, 60, 60); // 60x60
+        ViewGroup targetView2 = new FakeRootView(getTargetContext(), 20, 30, 40, 50); // 20x20
+        ViewGroup targetView3 = new FakeRootView(getTargetContext(), 5, 5, 15, 15); // 10x10
+
+        targetView1.addView(targetView2);
+        targetView2.addView(targetView3);
+
+        // Create first target with an unrelated parent
+        FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback();
+        callback1.setScrollBounds(new Rect(0, 0, 60, 60));
+        ScrollCaptureTarget target1 = createTargetWithView(targetView1, callback1,
+                new Rect(0, 0, 60, 60),
+                new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Create second target associated with a view within parent2
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTargetWithView(targetView2, callback2,
+                new Rect(0, 0, 20, 20),
+                new Point(20, 30), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        // Create third target associated with a view within parent3
+        FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback();
+        callback3.setScrollBounds(new Rect(0, 0, 15, 15));
+        ScrollCaptureTarget target3 = createTargetWithView(targetView3, callback3,
+                new Rect(0, 0, 15, 15),
+                new Point(25, 35), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        targetQueue.add(target1); // auto, 60x60
+        targetQueue.add(target2); // auto, 20x20
+        targetQueue.add(target3); // auto, 15x15 <- innermost scrollable
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        // Test only
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target3 as the result, due to relation", target3, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 15, 15), result.getScrollBounds());
+    }
+
+    /**
+     * If a timeout expires, late results are ignored.
+     */
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testTimeout() throws InterruptedException {
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+
+        // callback 1, 10x10, hint=AUTO, responds immediately from bg thread
+        BackgroundTestCallback callback1 = new BackgroundTestCallback();
+        callback1.setScrollBounds(new Rect(5, 5, 15, 15));
+        ScrollCaptureTarget target1 = createTarget(
+                callback1, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        targetQueue.add(target1);
+
+        // callback 2, 20x20, hint=AUTO, responds after 5s from bg thread
+        BackgroundTestCallback callback2 = new BackgroundTestCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        callback2.setDelay(5000);
+        ScrollCaptureTarget target2 = createTarget(
+                callback2, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_AUTO);
+        targetQueue.add(target2);
+
+        // callback 3, 20x20, hint=INCLUDE, responds after 10s from bg thread
+        BackgroundTestCallback callback3 = new BackgroundTestCallback();
+        callback3.setScrollBounds(new Rect(0, 0, 20, 20));
+        callback3.setDelay(10000);
+        ScrollCaptureTarget target3 = createTarget(
+                callback3, new Rect(20, 30, 40, 50), new Point(10, 10),
+                View.SCROLL_CAPTURE_HINT_INCLUDE);
+        targetQueue.add(target3);
+
+        // callback 1 will be received
+        // callback 2 & 3 will be ignored due to timeout
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target1 as the result, due to timeouts of others", target1, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(5, 5, 15, 15), result.getScrollBounds());
+        assertEquals("callback1 should have been called",
+                1, callback1.getOnScrollCaptureSearchCount());
+        assertEquals("callback2 should have been called",
+                1, callback2.getOnScrollCaptureSearchCount());
+        assertEquals("callback3 should have been called",
+                1, callback3.getOnScrollCaptureSearchCount());
+    }
+
+    @Test(timeout = TEST_TIMEOUT_MS)
+    public void testWithCallbackMultipleReplies() throws InterruptedException {
+        // Calls response methods 3 times each
+        RepeatingCaptureCallback callback1 = new RepeatingCaptureCallback(3);
+        callback1.setScrollBounds(new Rect(2, 2, 18, 18));
+        ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50),
+                new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback();
+        callback2.setScrollBounds(new Rect(0, 0, 20, 20));
+        ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50),
+                new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO);
+
+        LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>();
+        targetQueue.add(target1);
+        targetQueue.add(target2);
+
+        ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue);
+        resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer);
+
+        resolver.waitForResult();
+
+        ScrollCaptureTarget result = mTargetConsumer.getLastValue();
+        assertSame("Expected target2 as the result, due to hint=INCLUDE", target2, result);
+        assertEquals("result has wrong scroll bounds",
+                new Rect(0, 0, 20, 20), result.getScrollBounds());
+        assertEquals("callback1 should have been called once",
+                1, callback1.getOnScrollCaptureSearchCount());
+        assertEquals("callback2 should have been called once",
+                1, callback2.getOnScrollCaptureSearchCount());
+    }
+
+    private static class TargetConsumer implements Consumer<ScrollCaptureTarget> {
+        volatile ScrollCaptureTarget mResult;
+        int mAcceptCount;
+
+        ScrollCaptureTarget getLastValue() {
+            return mResult;
+        }
+
+        int acceptCount() {
+            return mAcceptCount;
+        }
+
+        @Override
+        public void accept(@Nullable ScrollCaptureTarget t) {
+            mAcceptCount++;
+            mResult = t;
+        }
+    }
+
+    private void setupTargetView(View view, Rect localVisibleRect, int scrollCaptureHint) {
+        view.setScrollCaptureHint(scrollCaptureHint);
+        view.onVisibilityAggregated(true);
+        // Treat any offset as padding, outset localVisibleRect on all sides and use this as
+        // child bounds
+        Rect bounds = new Rect(localVisibleRect);
+        bounds.inset(-bounds.left, -bounds.top, bounds.left, bounds.top);
+        view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
+        view.onVisibilityAggregated(true);
+    }
+
+    private ScrollCaptureTarget createTarget(ScrollCaptureCallback callback, Rect localVisibleRect,
+            Point positionInWindow, int scrollCaptureHint) {
+        View mockView = new View(getTargetContext());
+        return createTargetWithView(mockView, callback, localVisibleRect, positionInWindow,
+                scrollCaptureHint);
+    }
+
+    private ScrollCaptureTarget createTargetWithView(View view, ScrollCaptureCallback callback,
+            Rect localVisibleRect, Point positionInWindow, int scrollCaptureHint) {
+        setupTargetView(view, localVisibleRect, scrollCaptureHint);
+        return new ScrollCaptureTarget(view, localVisibleRect, positionInWindow, callback);
+    }
+
+
+    static class FakeRootView extends ViewGroup implements ViewParent {
+        FakeRootView(Context context, int l, int t, int r, int b) {
+            super(context);
+            layout(l, t, r, b);
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        }
+    }
+
+    static class FakeScrollCaptureCallback implements ScrollCaptureCallback {
+        private Rect mScrollBounds;
+        private long mDelayMillis;
+        private int mOnScrollCaptureSearchCount;
+
+        public int getOnScrollCaptureSearchCount() {
+            return mOnScrollCaptureSearchCount;
+        }
+
+        @Override
+        public void onScrollCaptureSearch(Consumer<Rect> onReady) {
+            mOnScrollCaptureSearchCount++;
+            run(() -> {
+                Rect b = getScrollBounds();
+                onReady.accept(b);
+            });
+        }
+
+        @Override
+        public void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) {
+            run(onReady);
+        }
+
+        @Override
+        public void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect captureArea) {
+            run(() -> session.notifyBufferSent(0, captureArea));
+        }
+
+        @Override
+        public void onScrollCaptureEnd(Runnable onReady) {
+            run(onReady);
+        }
+
+        public void setScrollBounds(@Nullable Rect scrollBounds) {
+            mScrollBounds = scrollBounds;
+        }
+
+        public void setDelay(long delayMillis) {
+            mDelayMillis = delayMillis;
+        }
+
+        protected Rect getScrollBounds() {
+            return mScrollBounds;
+        }
+
+        protected void run(Runnable r) {
+            delay();
+            r.run();
+        }
+
+        protected void delay() {
+            if (mDelayMillis > 0) {
+                try {
+                    Thread.sleep(mDelayMillis);
+                } catch (InterruptedException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    static class RepeatingCaptureCallback extends FakeScrollCaptureCallback {
+        private int mRepeatCount;
+
+        RepeatingCaptureCallback(int repeatCount) {
+            mRepeatCount = repeatCount;
+        }
+
+        protected void run(Runnable r) {
+            delay();
+            for (int i = 0; i < mRepeatCount; i++) {
+                r.run();
+            }
+        }
+    }
+
+    /** Response to async calls on an arbitrary background thread */
+    static class BackgroundTestCallback extends FakeScrollCaptureCallback {
+        static int sCount = 0;
+        private void runOnBackgroundThread(Runnable r) {
+            final Runnable target = () -> {
+                delay();
+                r.run();
+            };
+            Thread t = new Thread(target);
+            synchronized (BackgroundTestCallback.this) {
+                sCount++;
+            }
+            t.setName("Background-Thread-" + sCount);
+            t.start();
+        }
+
+        @Override
+        protected void run(Runnable r) {
+            runOnBackgroundThread(r);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java
new file mode 100644
index 0000000..3af0533e
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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 android.view;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertSame;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Exercises Scroll Capture search in {@link ViewGroup}.
+ */
+@Presubmit
+@SmallTest
+@FlakyTest(detail = "promote once confirmed flake-free")
+@RunWith(MockitoJUnitRunner.class)
+public class ViewGroupScrollCaptureTest {
+
+    @Mock
+    ScrollCaptureCallback mMockCallback;
+    @Mock
+    ScrollCaptureCallback mMockCallback2;
+
+    /** Make sure the hint flags are saved and loaded correctly. */
+    @Test
+    public void testSetScrollCaptureHint() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context);
+
+        assertNotNull(viewGroup);
+        assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]",
+                ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
+                | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
+                        | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+
+        viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
+                | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
+        assertEquals("The scroll capture hint was not stored correctly.",
+                ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
+                        | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
+                viewGroup.getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with 'scrollCaptureHint=auto', but no ScrollCaptureCallback set dispatches
+     * correctly. Verifies that the framework helper is called. Verifies a that non-null callback
+     * return results in an expected target in the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_noCallback_hintAuto() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Verify the system checked for fallback support
+        viewGroup.assertDispatchScrollCaptureCount(1);
+        viewGroup.assertLastDispatchScrollCaptureArgs(localVisibleRect, windowOffset);
+
+        // Verify the target is as expected.
+        assertEquals(1, targetList.size());
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
+        assertSame("Target has the wrong View", viewGroup, target.getContainingView());
+        assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with 'scrollCaptureHint=exclude' is ignored. The Framework helper is
+     * stubbed to return a callback. Verifies that the framework helper is not called (because of
+     * exclude), and no scroll capture target is added to the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_noCallback_hintExclude() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup =
+                new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Verify the results.
+        assertEquals("Target list size should be zero.", 0, targetList.size());
+    }
+
+    /**
+     * Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback set
+     * dispatches as expected. Also verifies that the system fallback support is not called, and the
+     * the returned target is constructed correctly.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_withCallback_hintAuto() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        // With an already provided scroll capture callback
+        viewGroup.setScrollCaptureCallback(mMockCallback);
+
+        // When system internal scroll capture is requested, this callback is returned.
+        viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Confirm that framework support was not requested,
+        // because this view already had a callback set.
+        viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // Verify the target is as expected.
+        assertEquals(1, targetList.size());
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("Target has the wrong callback", mMockCallback, target.getCallback());
+        assertSame("Target has the wrong View", viewGroup, target.getContainingView());
+        assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    /**
+     * Ensure a ViewGroup with a callback set, but 'scrollCaptureHint=exclude' is ignored. The
+     * exclude flag takes precedence.  Verifies that the framework helper is not called (because of
+     * exclude, and a callback being set), and no scroll capture target is added to the results.
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_withCallback_hintExclude() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        MockViewGroup viewGroup =
+                new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
+        // With an already provided scroll capture callback
+        viewGroup.setScrollCaptureCallback(mMockCallback);
+
+        Rect localVisibleRect = new Rect(0, 0, 200, 200);
+        Point windowOffset = new Point();
+        LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup itself
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // Confirm that framework support was not requested, because this view is excluded.
+        // (And because this view has a callback set.)
+        viewGroup.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // Has callback, but hint=excluded, so excluded.
+        assertTrue(targetList.isEmpty());
+    }
+
+    /**
+     * Test scroll capture search dispatch to child views.
+     * <p>
+     * Verifies computation of child visible bounds.
+     * TODO: with scrollX / scrollY, split up into discrete tests
+     */
+    @MediumTest
+    @Test
+    public void testDispatchScrollCaptureSearch_toChildren() throws Exception {
+        final Context context = getInstrumentation().getContext();
+        final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
+
+        Rect localVisibleRect = new Rect(25, 50, 175, 150);
+        Point windowOffset = new Point(0, 0);
+
+        //        visible area
+        //       |<- l=25,    |
+        //       |    r=175 ->|
+        // +--------------------------+
+        // | view1 (0, 0, 200, 25)    |
+        // +---------------+----------+
+        // |               |          |
+        // | view2         | view4    | --+
+        // | (0, 25,       |    (inv) |   | visible area
+        // |      150, 100)|          |   |
+        // +---------------+----------+   | t=50, b=150
+        // | view3         | view5    |   |
+        // | (0, 100       |(150, 100 | --+
+        // |     200, 200) | 200, 200)|
+        // |               |          |
+        // |               |          |
+        // +---------------+----------+ (200,200)
+
+        // View 1 is clipped and not visible.
+        final MockView view1 = new MockView(context, 0, 0, 200, 25);
+        viewGroup.addView(view1);
+
+        // View 2 is partially visible.
+        final MockView view2 = new MockView(context, 0, 25, 150, 100);
+        viewGroup.addView(view2);
+
+        // View 3 is partially visible.
+        // Pretend View3 can scroll by having framework provide fallback support
+        final MockView view3 = new MockView(context, 0, 100, 200, 200);
+        // When system internal scroll capture is requested for this view, return this callback.
+        view3.setScrollCaptureCallbackInternalForTest(mMockCallback);
+        viewGroup.addView(view3);
+
+        // View 4 is invisible and should be ignored.
+        final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE);
+        viewGroup.addView(view4);
+
+        // View 4 is invisible and should be ignored.
+        final MockView view5 = new MockView(context, 150, 100, 200, 200);
+        // When system internal scroll capture is requested for this view, return this callback.
+        view5.setScrollCaptureCallback(mMockCallback2);
+        view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
+        viewGroup.addView(view5);
+
+        // Where targets are added
+        final LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>();
+
+        // Dispatch to the ViewGroup
+        viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList);
+
+        // View 1 is entirely clipped by the parent and not visible, dispatch
+        // skips this view entirely.
+        view1.assertDispatchScrollCaptureSearchCount(0);
+        view1.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // View 2, verify the computed localVisibleRect and windowOffset are correctly transformed
+        // to the child coordinate space
+        view2.assertDispatchScrollCaptureSearchCount(1);
+        view2.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(25, 25, 150, 75), new Point(0, 25));
+        // No callback set, so the framework is asked for support
+        view2.assertCreateScrollCaptureCallbackInternalCount(1);
+
+        // View 3, verify the computed localVisibleRect and windowOffset are correctly transformed
+        // to the child coordinate space
+        view3.assertDispatchScrollCaptureSearchCount(1);
+        view3.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(25, 0, 175, 50), new Point(0, 100));
+        // No callback set, so the framework is asked for support
+        view3.assertCreateScrollCaptureCallbackInternalCount(1);
+
+        // view4 is invisible, so it should be skipped entirely.
+        view4.assertDispatchScrollCaptureSearchCount(0);
+        view4.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // view5 is partially visible
+        view5.assertDispatchScrollCaptureSearchCount(1);
+        view5.assertDispatchScrollCaptureSearchLastArgs(
+                new Rect(0, 0, 25, 50), new Point(150, 100));
+        // view5 has a callback set on it, so internal framework support should not be consulted.
+        view5.assertCreateScrollCaptureCallbackInternalCount(0);
+
+        // 2 views should have been returned, view3 & view5
+        assertEquals(2, targetList.size());
+
+        ScrollCaptureTarget target = targetList.get(0);
+        assertSame("First target has the wrong View", view3, target.getContainingView());
+        assertSame("First target has the wrong callback", mMockCallback, target.getCallback());
+        assertEquals("First target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
+                target.getContainingView().getScrollCaptureHint());
+
+        target = targetList.get(1);
+        assertSame("Second target has the wrong View", view5, target.getContainingView());
+        assertSame("Second target has the wrong callback", mMockCallback2, target.getCallback());
+        assertEquals("Second target hint is incorrect", View.SCROLL_CAPTURE_HINT_INCLUDE,
+                target.getContainingView().getScrollCaptureHint());
+    }
+
+    public static final class MockView extends View {
+        private ScrollCaptureCallback mInternalCallback;
+
+        private int mDispatchScrollCaptureSearchNumCalls;
+        private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
+        private Point mDispatchScrollCaptureSearchLastWindowOffset;
+        private int mCreateScrollCaptureCallbackInternalCount;
+
+        MockView(Context context) {
+            this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
+        }
+
+        MockView(Context context, int left, int top, int right, int bottom) {
+            this(context, left, top, right, bottom, View.VISIBLE);
+        }
+
+        MockView(Context context, int left, int top, int right, int bottom, int visibility) {
+            super(context);
+            setVisibility(visibility);
+            setFrame(left, top, right, bottom);
+        }
+
+        public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
+            mInternalCallback = internal;
+        }
+
+        void assertDispatchScrollCaptureSearchCount(int count) {
+            assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
+                    count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
+            assertEquals("arg localVisibleRect was incorrect.",
+                    localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
+            assertEquals("arg windowOffset was incorrect.",
+                    windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
+        }
+
+        void assertCreateScrollCaptureCallbackInternalCount(int count) {
+            assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
+                    count, mCreateScrollCaptureCallbackInternalCount);
+        }
+
+        void reset() {
+            mDispatchScrollCaptureSearchNumCalls = 0;
+            mDispatchScrollCaptureSearchLastWindowOffset = null;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
+            mCreateScrollCaptureCallbackInternalCount = 0;
+
+        }
+
+        @Override
+        public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
+                Queue<ScrollCaptureTarget> targets) {
+            mDispatchScrollCaptureSearchNumCalls++;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
+            mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
+            super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+        }
+
+        @Override
+        @Nullable
+        public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
+                Point offsetInWindow) {
+            mCreateScrollCaptureCallbackInternalCount++;
+            return mInternalCallback;
+        }
+    }
+
+    public static final class MockViewGroup extends ViewGroup {
+        private ScrollCaptureCallback mInternalCallback;
+        private int mDispatchScrollCaptureSearchNumCalls;
+        private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
+        private Point mDispatchScrollCaptureSearchLastWindowOffset;
+        private int mCreateScrollCaptureCallbackInternalCount;
+
+
+        MockViewGroup(Context context) {
+            this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
+        }
+
+        MockViewGroup(Context context, int left, int top, int right, int bottom) {
+            this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO);
+        }
+
+        MockViewGroup(Context context, int left, int top, int right, int bottom,
+                int scrollCaptureHint) {
+            super(context);
+            setScrollCaptureHint(scrollCaptureHint);
+            setFrame(left, top, right, bottom);
+        }
+
+        public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) {
+            mInternalCallback = internal;
+        }
+
+        void assertDispatchScrollCaptureSearchCount(int count) {
+            assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
+                    count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        @Override
+        @Nullable
+        public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect,
+                Point offsetInWindow) {
+            mCreateScrollCaptureCallbackInternalCount++;
+            return mInternalCallback;
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+            // We don't layout this view.
+        }
+
+        void assertDispatchScrollCaptureCount(int count) {
+            assertEquals(count, mDispatchScrollCaptureSearchNumCalls);
+        }
+
+        void assertLastDispatchScrollCaptureArgs(Rect localVisibleRect, Point windowOffset) {
+            assertEquals("arg localVisibleRect to dispatchScrollCaptureCallback was incorrect.",
+                    localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
+            assertEquals("arg windowOffset to dispatchScrollCaptureCallback was incorrect.",
+                    windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
+        }
+        void assertCreateScrollCaptureCallbackInternalCount(int count) {
+            assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal",
+                    count, mCreateScrollCaptureCallbackInternalCount);
+        }
+
+        void reset() {
+            mDispatchScrollCaptureSearchNumCalls = 0;
+            mDispatchScrollCaptureSearchLastWindowOffset = null;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
+            mCreateScrollCaptureCallbackInternalCount = 0;
+        }
+
+        @Override
+        public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
+                Queue<ScrollCaptureTarget> targets) {
+            mDispatchScrollCaptureSearchNumCalls++;
+            mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
+            mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
+            super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets);
+        }
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
index 769c578..5424b6f 100644
--- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java
@@ -648,12 +648,6 @@
         }
 
         @Override
-        public void startActivity(Intent intent) {
-            mStartActivityIntent = intent;
-            mUserIdActivityLaunchedIn = getUserId();
-        }
-
-        @Override
         protected MetricsLogger getMetricsLogger() {
             return mMetricsLogger;
         }
diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
new file mode 100644
index 0000000..63a68e9
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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 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 androidx.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+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 org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Random;
+
+public class ScrollViewCaptureHelperTest {
+
+    private FrameLayout mParent;
+    private ScrollView mTarget;
+    private LinearLayout mContent;
+    private WindowManager mWm;
+
+    private WindowManager.LayoutParams mWindowLayoutParams;
+
+    private static final int CHILD_VIEWS = 12;
+    public 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 Random mRandom;
+
+    private static float sDensity;
+
+    @BeforeClass
+    public static void setUpClass() {
+        sDensity = getContext().getResources().getDisplayMetrics().density;
+    }
+
+    @Before
+    @UiThreadTest
+    public void setUp() {
+        mRandom = new Random();
+        mParent = new FrameLayout(getContext());
+
+        mTarget = new ScrollView(getContext());
+        mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+
+        mContent = new LinearLayout(getContext());
+        mContent.setOrientation(LinearLayout.VERTICAL);
+        mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+
+        for (int i = 0; i < CHILD_VIEWS; i++) {
+            TextView view = new TextView(getContext());
+            view.setText("Child #" + 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));
+        }
+
+        // Window -> Parent -> Target -> Content
+
+        mWm = getContext().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);
+    }
+
+    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 assertCapturedAreaCompletelyVisible(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 assertCapturedAreaPartiallyVisible(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);
+        }
+    }
+
+    @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);
+
+        Rect result = 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(result);
+    }
+
+    @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);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+
+    @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);
+
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @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);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @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);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+        assertRectEquals(request, result);
+
+        assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @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);
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, 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(result);
+    }
+
+    @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));
+
+        Rect 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);
+        assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
+    }
+
+    @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));
+
+        Rect result = svc.onScrollRequested(mTarget, scrollBounds, request);
+
+        Rect expectedResult = new Rect(request);
+        expectedResult.bottom -= 300; // bottom half clipped
+        assertRectEquals(expectedResult, result);
+        assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent));
+
+    }
+
+    @Test
+    @UiThreadTest
+    public void onPrepareForEnd() {
+        ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper();
+        svc.onPrepareForEnd(mTarget);
+    }
+}
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 18086ec..c5ac451 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -283,6 +283,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "-1517908912": {
+      "message": "requestScrollCapture: caught exception dispatching to window.token=%s",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-1515151503": {
       "message": ">>> OPEN TRANSACTION removeReplacedWindows",
       "level": "INFO",
@@ -1441,6 +1447,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "646981048": {
+      "message": "Invalid displayId for requestScrollCapture: %d",
+      "level": "ERROR",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "662572728": {
       "message": "Attempted to add a toast window with bad token %s.  Aborting.",
       "level": "WARN",
@@ -1597,6 +1609,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1046922686": {
+      "message": "requestScrollCapture: caught exception dispatching callback: %s",
+      "level": "WARN",
+      "group": "WM_ERROR",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "1051545910": {
       "message": "Exit animation finished in %s: remove=%b",
       "level": "VERBOSE",
diff --git a/data/keyboards/Vendor_18d1_Product_0200.kcm b/data/keyboards/Vendor_18d1_Product_0200.kcm
new file mode 100644
index 0000000..231fac6
--- /dev/null
+++ b/data/keyboards/Vendor_18d1_Product_0200.kcm
@@ -0,0 +1,48 @@
+# 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.
+
+type FULL
+
+key BUTTON_A {
+    base:                               fallback DPAD_CENTER
+}
+
+key BUTTON_B {
+    base:                               fallback BACK
+}
+
+key BUTTON_X {
+    base:                               fallback DPAD_CENTER
+}
+
+key BUTTON_Y {
+    base:                               fallback BACK
+}
+
+key BUTTON_THUMBL {
+    base:                               fallback DPAD_CENTER
+}
+
+key BUTTON_THUMBR {
+    base:                               fallback DPAD_CENTER
+}
+
+key BUTTON_SELECT {
+    base:                               fallback MENU
+}
+
+key BUTTON_MODE {
+    base:                               fallback MENU
+}
+
diff --git a/data/keyboards/Vendor_18d1_Product_0200.kl b/data/keyboards/Vendor_18d1_Product_0200.kl
new file mode 100644
index 0000000..d30bcc6
--- /dev/null
+++ b/data/keyboards/Vendor_18d1_Product_0200.kl
@@ -0,0 +1,71 @@
+# 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.
+
+#
+# Keyboard map for the android virtual remote running as a gamepad
+#
+
+key 0x130 BUTTON_A
+key 0x131 BUTTON_B
+key 0x133 BUTTON_X
+key 0x134 BUTTON_Y
+
+key 0x136 BUTTON_L2
+key 0x137 BUTTON_R2
+key 0x138 BUTTON_L1
+key 0x139 BUTTON_R1
+
+key 0x13a BUTTON_SELECT
+key 0x13b BUTTON_START
+key 0x13c BUTTON_MODE
+
+key 0x13d BUTTON_THUMBL
+key 0x13e BUTTON_THUMBR
+
+key 103 DPAD_UP
+key 108 DPAD_DOWN
+key 105 DPAD_LEFT
+key 106 DPAD_RIGHT
+
+# Generic usage buttons
+key 0x2c0 BUTTON_1
+key 0x2c1 BUTTON_2
+key 0x2c2 BUTTON_3
+key 0x2c3 BUTTON_4
+key 0x2c4 BUTTON_5
+key 0x2c5 BUTTON_6
+key 0x2c6 BUTTON_7
+key 0x2c7 BUTTON_8
+key 0x2c8 BUTTON_9
+key 0x2c9 BUTTON_10
+key 0x2ca BUTTON_11
+key 0x2cb BUTTON_12
+key 0x2cc BUTTON_13
+key 0x2cd BUTTON_14
+key 0x2ce BUTTON_15
+key 0x2cf BUTTON_16
+
+# assistant buttons
+key 0x246 VOICE_ASSIST
+key 0x247 ASSIST
+
+axis 0x00 X
+axis 0x01 Y
+axis 0x02 Z
+axis 0x05 RZ
+axis 0x09 RTRIGGER
+axis 0x0a LTRIGGER
+axis 0x10 HAT_X
+axis 0x11 HAT_Y
+
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index cd878c5..228d03a 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -120,7 +120,7 @@
 
     /**
      * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
-     *
+     * <p>
      * In single buffered mode the application is responsible for serializing access to the image
      * content buffer. Each time the image content is to be updated, the
      * {@link #releaseTexImage()} method must be called before the image content producer takes
@@ -143,7 +143,7 @@
 
     /**
      * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
-     *
+     * <p>
      * In single buffered mode the application is responsible for serializing access to the image
      * content buffer. Each time the image content is to be updated, the
      * {@link #releaseTexImage()} method must be called before the image content producer takes
@@ -152,7 +152,7 @@
      * must be called before each ANativeWindow_lock, or that call will fail. When producing
      * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first
      * OpenGL ES function call each frame.
-     *
+     * <p>
      * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name,
      * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed
      * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing
@@ -222,15 +222,15 @@
      * method.  Both video and camera based image producers do override the size.  This method may
      * be used to set the image size when producing images with {@link android.graphics.Canvas} (via
      * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface).
-     *
+     * <p>
      * The new default buffer size will take effect the next time the image producer requests a
      * buffer to fill.  For {@link android.graphics.Canvas} this will be the next time {@link
      * android.view.Surface#lockCanvas} is called.  For OpenGL ES, the EGLSurface should be
      * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated
-     * (via eglCreateWindowSurface) to ensure that the new default size has taken effect.
-     *
+     * (via {@code eglCreateWindowSurface}) to ensure that the new default size has taken effect.
+     * <p>
      * The width and height parameters must be no greater than the minimum of
-     * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see
+     * {@code GL_MAX_VIEWPORT_DIMS} and {@code GL_MAX_TEXTURE_SIZE} (see
      * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}).
      * An error due to invalid dimensions might not be reported until
      * updateTexImage() is called.
@@ -242,7 +242,7 @@
     /**
      * Update the texture image to the most recent frame from the image stream.  This may only be
      * called while the OpenGL ES context that owns the texture is current on the calling thread.
-     * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.
+     * It will implicitly bind its texture to the {@code GL_TEXTURE_EXTERNAL_OES} texture target.
      */
     public void updateTexImage() {
         nativeUpdateTexImage();
@@ -251,6 +251,7 @@
     /**
      * Releases the the texture content. This is needed in single buffered mode to allow the image
      * content producer to take ownership of the image buffer.
+     * <p>
      * For more information see {@link #SurfaceTexture(int, boolean)}.
      */
     public void releaseTexImage() {
@@ -263,7 +264,7 @@
      * ES texture object will be deleted as a result of this call.  After calling this method all
      * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until
      * a successful call to {@link #attachToGLContext} is made.
-     *
+     * <p>
      * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
      * contexts.  Note, however, that the image contents are only accessible from one OpenGL ES
      * context at a time.
@@ -279,8 +280,8 @@
      * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread.  A
      * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame
      * that was current at the time of the last call to {@link #detachFromGLContext}.  This new
-     * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target.
-     *
+     * texture is bound to the {@code GL_TEXTURE_EXTERNAL_OES} texture target.
+     * <p>
      * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES
      * contexts.  Note, however, that the image contents are only accessible from one OpenGL ES
      * context at a time.
@@ -297,16 +298,16 @@
 
     /**
      * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by
-     * the most recent call to updateTexImage.
-     *
+     * the most recent call to {@link #updateTexImage}.
+     * <p>
      * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s
      * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample
      * that location from the texture.  Sampling the texture outside of the range of this transform
      * is undefined.
-     *
+     * <p>
      * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via
-     * the glLoadMatrixf or glUniformMatrix4fv functions.
-     *
+     * the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions.
+     * <p>
      * If the underlying buffer has a crop associated with it, the transformation will also include
      * a slight scale to cut off a 1-texel border around the edge of the crop. This ensures that
      * when the texture is bilinear sampled that no texels outside of the buffer's valid region
@@ -326,7 +327,7 @@
 
     /**
      * Retrieve the timestamp associated with the texture image set by the most recent call to
-     * updateTexImage.
+     * {@link #updateTexImage}.
      *
      * <p>This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp
      * should be unaffected by time-of-day adjustments. The specific meaning and zero point of the
@@ -337,8 +338,8 @@
      *
      * <p>For camera sources, timestamps should be strictly monotonic. Timestamps from MediaPlayer
      * sources may be reset when the playback position is set. For EGL and Vulkan producers, the
-     * timestamp is the desired present time set with the EGL_ANDROID_presentation_time or
-     * VK_GOOGLE_display_timing extensions.</p>
+     * timestamp is the desired present time set with the {@code EGL_ANDROID_presentation_time} or
+     * {@code VK_GOOGLE_display_timing} extensions.</p>
      */
 
     public long getTimestamp() {
@@ -346,16 +347,17 @@
     }
 
     /**
-     * release() frees all the buffers and puts the SurfaceTexture into the
+     * {@code release()} frees all the buffers and puts the SurfaceTexture into the
      * 'abandoned' state. Once put in this state the SurfaceTexture can never
      * leave it. When in the 'abandoned' state, all methods of the
-     * IGraphicBufferProducer interface will fail with the NO_INIT error.
-     *
+     * {@code IGraphicBufferProducer} interface will fail with the {@code NO_INIT}
+     * error.
+     * <p>
      * Note that while calling this method causes all the buffers to be freed
      * from the perspective of the the SurfaceTexture, if there are additional
      * references on the buffers (e.g. if a buffer is referenced by a client or
      * by OpenGL ES as a texture) then those buffer will remain allocated.
-     *
+     * <p>
      * Always call this method when you are done with SurfaceTexture. Failing
      * to do so may delay resource deallocation for a significant amount of
      * time.
@@ -367,7 +369,7 @@
     }
 
     /**
-     * Returns true if the SurfaceTexture was released.
+     * Returns {@code true} if the SurfaceTexture was released.
      *
      * @see #release()
      */
@@ -400,7 +402,7 @@
     }
 
     /**
-     * Returns true if the SurfaceTexture is single-buffered
+     * Returns {@code true} if the SurfaceTexture is single-buffered.
      * @hide
      */
     public boolean isSingleBuffered() {
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl
index dc06153..a8b82ba 100644
--- a/media/java/android/media/IMediaRouter2.aidl
+++ b/media/java/android/media/IMediaRouter2.aidl
@@ -24,11 +24,15 @@
  * @hide
  */
 oneway interface IMediaRouter2 {
-    void notifyRestoreRoute();
     void notifyRoutesAdded(in List<MediaRoute2Info> routes);
     void notifyRoutesRemoved(in List<MediaRoute2Info> routes);
     void notifyRoutesChanged(in List<MediaRoute2Info> routes);
     void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo);
     void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo);
     void notifySessionReleased(in RoutingSessionInfo sessionInfo);
+    /**
+     * Gets hints of the new session for the given route.
+     * Call MediaRouterService#notifySessionHintsForCreatingSession to pass the result.
+     */
+    void getSessionHintsForCreatingSession(long uniqueRequestId, in MediaRoute2Info route);
 }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 0d87736..52bac67 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -59,6 +59,8 @@
 
     void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId,
             in MediaRoute2Info route, in @nullable Bundle sessionHints);
+    void notifySessionHintsForCreatingSession(IMediaRouter2 router, long uniqueRequestId,
+                in MediaRoute2Info route, in @nullable Bundle sessionHints);
     void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route);
     void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId,
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index cbf2364..1bfa999 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -1312,7 +1312,7 @@
     }
 
     /**
-     * Returns the value of an long key, or the default value if the key is missing.
+     * Returns the value of a long key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, float, ByteBuffer or
@@ -1340,19 +1340,15 @@
     }
 
     /**
-     * Returns the value of an float key, or the default value if the key is missing.
+     * Returns the value of a float key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, long, ByteBuffer or
      *         String
      */
     public final float getFloat(@NonNull String name, float defaultValue) {
-        try {
-            return getFloat(name);
-        } catch (NullPointerException  e) {
-            /* no such field or field is null */
-            return defaultValue;
-        }
+        Object value = mMap.get(name);
+        return value != null ? (float) value : defaultValue;
     }
 
     /**
@@ -1366,7 +1362,7 @@
     }
 
     /**
-     * Returns the value of an string key, or the default value if the key is missing.
+     * Returns the value of a string key, or the default value if the key is missing.
      *
      * @return defaultValue if the key does not exist or the stored value for the key is null
      * @throws ClassCastException if the stored value for the key is int, long, float or ByteBuffer
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index bd8fb96..0ea9624 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -237,9 +237,9 @@
                 } catch (RemoteException ex) {
                     Log.e(TAG, "Unable to unregister media router.", ex);
                 }
+                mStub = null;
             }
             mShouldUpdateRoutes = true;
-            mStub = null;
         }
     }
 
@@ -690,6 +690,31 @@
         matchingController.releaseInternal(/* shouldReleaseSession= */ false);
     }
 
+    void onGetControllerHintsForCreatingSessionOnHandler(long uniqueRequestId,
+            MediaRoute2Info route) {
+        OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
+        Bundle controllerHints = null;
+        if (listener != null) {
+            controllerHints = listener.onGetControllerHints(route);
+            if (controllerHints != null) {
+                controllerHints = new Bundle(controllerHints);
+            }
+        }
+
+        MediaRouter2Stub stub;
+        synchronized (sRouterLock) {
+            stub = mStub;
+        }
+        if (stub != null) {
+            try {
+                mMediaRouterService.notifySessionHintsForCreatingSession(
+                        stub, uniqueRequestId, route, controllerHints);
+            } catch (RemoteException ex) {
+                Log.e(TAG, "getSessionHintsOnHandler: Unable to request.", ex);
+            }
+        }
+    }
+
     private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes,
             RouteDiscoveryPreference discoveryRequest) {
         return routes.stream()
@@ -820,13 +845,14 @@
      */
     public interface OnGetControllerHintsListener {
         /**
-         * Called when the {@link MediaRouter2} is about to request
-         * the media route provider service to create a controller with the given route.
+         * Called when the {@link MediaRouter2} or the system is about to request
+         * a media route provider service to create a controller with the given route.
          * The {@link Bundle} returned here will be sent to media route provider service as a hint.
          * <p>
-         * To send hints when creating the controller, set the listener before calling
-         * {@link #transferTo(MediaRoute2Info)}. The method will be called
-         * on the same thread which calls {@link #transferTo(MediaRoute2Info)}.
+         * Since controller creation can be requested by the {@link MediaRouter2} and the system,
+         * set the listener as soon as possible after acquiring {@link MediaRouter2} instance.
+         * The method will be called on the same thread that calls
+         * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system.
          *
          * @param route The route to create controller with
          * @return An optional bundle of app-specific arguments to send to the provider,
@@ -1378,9 +1404,6 @@
 
     class MediaRouter2Stub extends IMediaRouter2.Stub {
         @Override
-        public void notifyRestoreRoute() throws RemoteException {}
-
-        @Override
         public void notifyRoutesAdded(List<MediaRoute2Info> routes) {
             mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler,
                     MediaRouter2.this, routes));
@@ -1415,5 +1438,13 @@
             mHandler.sendMessage(obtainMessage(MediaRouter2::releaseControllerOnHandler,
                     MediaRouter2.this, sessionInfo));
         }
+
+        @Override
+        public void getSessionHintsForCreatingSession(long uniqueRequestId,
+                @NonNull MediaRoute2Info route) {
+            mHandler.sendMessage(obtainMessage(
+                    MediaRouter2::onGetControllerHintsForCreatingSessionOnHandler,
+                    MediaRouter2.this, uniqueRequestId, route));
+        }
     }
 }
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index b694fd0..3b570b6 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -147,14 +147,16 @@
         }
 
         synchronized (sLock) {
-            if (mCallbackRecords.size() == 0 && mClient != null) {
-                try {
-                    mMediaRouterService.unregisterManager(mClient);
-                } catch (RemoteException ex) {
-                    Log.e(TAG, "Unable to unregister media router manager", ex);
+            if (mCallbackRecords.size() == 0) {
+                if (mClient != null) {
+                    try {
+                        mMediaRouterService.unregisterManager(mClient);
+                    } catch (RemoteException ex) {
+                        Log.e(TAG, "Unable to unregister media router manager", ex);
+                    }
+                    mClient = null;
                 }
-                //TODO: clear mRoutes?
-                mClient = null;
+                mRoutes.clear();
                 mPreferredFeaturesMap.clear();
             }
         }
diff --git a/media/java/android/media/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
index a0b6c9b..0e6563a 100644
--- a/media/java/android/media/tv/ITvRemoteServiceInput.aidl
+++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl
@@ -39,4 +39,10 @@
     void sendPointerUp(IBinder token, int pointerId);
     @UnsupportedAppUsage
     void sendPointerSync(IBinder token);
-}
\ No newline at end of file
+
+    // API specific to gamepads. Close gamepads with closeInputBridge
+    void openGamepadBridge(IBinder token, String name);
+    void sendGamepadKeyDown(IBinder token, int keyCode);
+    void sendGamepadKeyUp(IBinder token, int keyCode);
+    void sendGamepadAxisValue(IBinder token, int axis, float value);
+}
diff --git a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java
index 0bf0f97..b97ac26 100644
--- a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java
+++ b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.media.tv.remoteprovider;
 
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.media.tv.ITvRemoteProvider;
 import android.media.tv.ITvRemoteServiceInput;
@@ -24,6 +26,7 @@
 import android.util.Log;
 
 import java.util.LinkedList;
+import java.util.Objects;
 
 /**
  * Base class for emote providers implemented in unbundled service.
@@ -124,27 +127,75 @@
      * @param maxPointers Maximum supported pointers
      * @throws RuntimeException
      */
-    public void openRemoteInputBridge(IBinder token, String name, int width, int height,
-                                      int maxPointers) throws RuntimeException {
+    public void openRemoteInputBridge(
+            IBinder token, String name, int width, int height, int maxPointers)
+            throws RuntimeException {
+        final IBinder finalToken = Objects.requireNonNull(token);
+        final String finalName = Objects.requireNonNull(name);
+
         synchronized (mOpenBridgeRunnables) {
             if (mRemoteServiceInput == null) {
-                Log.d(TAG, "Delaying openRemoteInputBridge() for " + name);
+                Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName);
 
                 mOpenBridgeRunnables.add(() -> {
                     try {
                         mRemoteServiceInput.openInputBridge(
-                                token, name, width, height, maxPointers);
-                        Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success");
+                                finalToken, finalName, width, height, maxPointers);
+                        Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName
+                                + ": success");
                     } catch (RemoteException re) {
-                        Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re);
+                        Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName
+                                + ": failure", re);
                     }
                 });
                 return;
             }
         }
         try {
-            mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers);
-            Log.d(TAG, "openRemoteInputBridge() for " + name + ": success");
+            mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers);
+            Log.d(TAG, "openRemoteInputBridge() for " + finalName + ": success");
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Opens an input bridge as a gamepad device.
+     * Clients should pass in a token that can be used to match this request with a token that
+     * will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)}
+     * <p>
+     * The token should be used for subsequent calls.
+     * </p>
+     *
+     * @param token       Identifier for this connection
+     * @param name        Device name
+     * @throws RuntimeException
+     *
+     * @hide
+     */
+    public void openGamepadBridge(@NonNull IBinder token, @NonNull  String name)
+            throws RuntimeException {
+        final IBinder finalToken = Objects.requireNonNull(token);
+        final String finalName = Objects.requireNonNull(name);
+        synchronized (mOpenBridgeRunnables) {
+            if (mRemoteServiceInput == null) {
+                Log.d(TAG, "Delaying openGamepadBridge() for " + finalName);
+
+                mOpenBridgeRunnables.add(() -> {
+                    try {
+                        mRemoteServiceInput.openGamepadBridge(finalToken, finalName);
+                        Log.d(TAG, "Delayed openGamepadBridge() for " + finalName + ": success");
+                    } catch (RemoteException re) {
+                        Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure",
+                                re);
+                    }
+                });
+                return;
+            }
+        }
+        try {
+            mRemoteServiceInput.openGamepadBridge(token, finalName);
+            Log.d(TAG, "openGamepadBridge() for " + finalName + ": success");
         } catch (RemoteException re) {
             throw re.rethrowFromSystemServer();
         }
@@ -157,6 +208,7 @@
      * @throws RuntimeException
      */
     public void closeInputBridge(IBinder token) throws RuntimeException {
+        Objects.requireNonNull(token);
         try {
             mRemoteServiceInput.closeInputBridge(token);
         } catch (RemoteException re) {
@@ -173,6 +225,7 @@
      * @throws RuntimeException
      */
     public void clearInputBridge(IBinder token) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token);
         try {
             mRemoteServiceInput.clearInputBridge(token);
@@ -190,6 +243,7 @@
      * @throws RuntimeException
      */
     public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token +
                 ", timestamp: " + timestamp);
         try {
@@ -207,6 +261,7 @@
      * @throws RuntimeException
      */
     public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode);
         try {
             mRemoteServiceInput.sendKeyUp(token, keyCode);
@@ -223,6 +278,7 @@
      * @throws RuntimeException
      */
     public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token +
                 ", keyCode: " + keyCode);
         try {
@@ -241,6 +297,7 @@
      * @throws RuntimeException
      */
     public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token +
                 ", pointerId: " + pointerId);
         try {
@@ -262,6 +319,7 @@
      */
     public void sendPointerDown(IBinder token, int pointerId, int x, int y)
             throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token +
                 ", pointerId: " + pointerId);
         try {
@@ -278,6 +336,7 @@
      * @throws RuntimeException
      */
     public void sendPointerSync(IBinder token) throws RuntimeException {
+        Objects.requireNonNull(token);
         if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token);
         try {
             mRemoteServiceInput.sendPointerSync(token);
@@ -286,6 +345,94 @@
         }
     }
 
+    /**
+     * Send a notification that a gamepad key was pressed.
+     *
+     * Supported buttons are:
+     * <ul>
+     *   <li> Right-side buttons: BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y
+     *   <li> Digital Triggers and bumpers: BUTTON_L1, BUTTON_R1, BUTTON_L2, BUTTON_R2
+     *   <li> Thumb buttons: BUTTON_THUMBL, BUTTON_THUMBR
+     *   <li> DPad buttons: DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT
+     *   <li> Gamepad buttons: BUTTON_SELECT, BUTTON_START, BUTTON_MODE
+     *   <li> Generic buttons: BUTTON_1, BUTTON_2, ...., BUTTON16
+     *   <li> Assistant: ASSIST, VOICE_ASSIST
+     * </ul>
+     *
+     * @param token   identifier for the device
+     * @param keyCode the gamepad key that was pressed (like BUTTON_A)
+     *
+     * @hide
+     */
+    public void sendGamepadKeyDown(@NonNull IBinder token, int keyCode) throws RuntimeException {
+        Objects.requireNonNull(token);
+        if (DEBUG_KEYS) {
+            Log.d(TAG, "sendGamepadKeyDown() token: " + token);
+        }
+
+        try {
+            mRemoteServiceInput.sendGamepadKeyDown(token, keyCode);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Send a notification that a gamepad key was released.
+     *
+     * @see sendGamepadKeyDown for supported key codes.
+     *
+     * @param token identifier for the device
+     * @param keyCode the gamepad key that was pressed
+     *
+     * @hide
+     */
+    public void sendGamepadKeyUp(@NonNull IBinder token, int keyCode) throws RuntimeException {
+        Objects.requireNonNull(token);
+        if (DEBUG_KEYS) {
+            Log.d(TAG, "sendGamepadKeyUp() token: " + token);
+        }
+
+        try {
+            mRemoteServiceInput.sendGamepadKeyUp(token, keyCode);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Send a gamepad axis value.
+     *
+     * Supported axes:
+     *  <li> Left Joystick: AXIS_X, AXIS_Y
+     *  <li> Right Joystick: AXIS_Z, AXIS_RZ
+     *  <li> Triggers: AXIS_LTRIGGER, AXIS_RTRIGGER
+     *  <li> DPad: AXIS_HAT_X, AXIS_HAT_Y
+     *
+     * For non-trigger axes, the range of acceptable values is [-1, 1]. The trigger axes support
+     * values [0, 1].
+     *
+     * @param token identifier for the device
+     * @param axis  MotionEvent axis
+     * @param value the value to send
+     *
+     * @hide
+     */
+    public void sendGamepadAxisValue(
+            @NonNull IBinder token, int axis, @FloatRange(from = -1.0f, to = 1.0f) float value)
+            throws RuntimeException {
+        Objects.requireNonNull(token);
+        if (DEBUG_KEYS) {
+            Log.d(TAG, "sendGamepadAxisValue() token: " + token);
+        }
+
+        try {
+            mRemoteServiceInput.sendGamepadAxisValue(token, axis, value);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
     private final class ProviderStub extends ITvRemoteProvider.Stub {
         @Override
         public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) {
diff --git a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java
index c9ce5613..e6e3939 100644
--- a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java
+++ b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java
@@ -83,4 +83,52 @@
 
         assertTrue(tvProvider.verifyTokens());
     }
+
+    @SmallTest
+    public void testOpenGamepadRemoteInputBridge() throws Exception {
+        Binder tokenA = new Binder();
+        Binder tokenB = new Binder();
+        Binder tokenC = new Binder();
+
+        class LocalTvRemoteProvider extends TvRemoteProvider {
+            private final ArrayList<IBinder> mTokens = new ArrayList<IBinder>();
+
+            LocalTvRemoteProvider(Context context) {
+                super(context);
+            }
+
+            @Override
+            public void onInputBridgeConnected(IBinder token) {
+                mTokens.add(token);
+            }
+
+            public boolean verifyTokens() {
+                return mTokens.size() == 3 && mTokens.contains(tokenA) && mTokens.contains(tokenB)
+                        && mTokens.contains(tokenC);
+            }
+        }
+
+        LocalTvRemoteProvider tvProvider = new LocalTvRemoteProvider(getContext());
+        ITvRemoteProvider binder = (ITvRemoteProvider) tvProvider.getBinder();
+
+        ITvRemoteServiceInput tvServiceInput = mock(ITvRemoteServiceInput.class);
+        doAnswer((i) -> {
+            binder.onInputBridgeConnected(i.getArgument(0));
+            return null;
+        })
+                .when(tvServiceInput)
+                .openGamepadBridge(any(), any());
+
+        tvProvider.openGamepadBridge(tokenA, "A");
+        tvProvider.openGamepadBridge(tokenB, "B");
+        binder.setRemoteServiceInputSink(tvServiceInput);
+        tvProvider.openGamepadBridge(tokenC, "C");
+
+        verify(tvServiceInput).openGamepadBridge(tokenA, "A");
+        verify(tvServiceInput).openGamepadBridge(tokenB, "B");
+        verify(tvServiceInput).openGamepadBridge(tokenC, "C");
+        verifyNoMoreInteractions(tvServiceInput);
+
+        assertTrue(tvProvider.verifyTokens());
+    }
 }
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index 6ca564f..6a1e965 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -36,6 +36,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
@@ -47,6 +48,7 @@
 import android.media.MediaRouter2Utils;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -73,6 +75,8 @@
     private static final String TAG = "MediaRouter2ManagerTest";
     private static final int WAIT_TIME_MS = 2000;
     private static final int TIMEOUT_MS = 5000;
+    private static final String TEST_KEY = "test_key";
+    private static final String TEST_VALUE = "test_value";
 
     private Context mContext;
     private MediaRouter2Manager mManager;
@@ -160,6 +164,7 @@
         });
 
         MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2);
+        assertNotNull(routeToRemove);
 
         StubMediaRoute2ProviderService sInstance =
                 StubMediaRoute2ProviderService.getInstance();
@@ -171,6 +176,52 @@
         assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void testGetRoutes_removedRoute_returnsCorrectRoutes() throws Exception {
+        CountDownLatch addedLatch = new CountDownLatch(1);
+        CountDownLatch removedLatch = new CountDownLatch(1);
+
+        RouteCallback routeCallback = new RouteCallback() {
+            // Used to ensure the removed route is added.
+            @Override
+            public void onRoutesAdded(List<MediaRoute2Info> routes) {
+                if (removedLatch.getCount() > 0) {
+                    return;
+                }
+                addedLatch.countDown();
+            }
+
+            @Override
+            public void onRoutesRemoved(List<MediaRoute2Info> routes) {
+                removedLatch.countDown();
+            }
+        };
+
+        mRouter2.registerRouteCallback(mExecutor, routeCallback,
+                new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build());
+        mRouteCallbacks.add(routeCallback);
+
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2);
+        assertNotNull(routeToRemove);
+
+        StubMediaRoute2ProviderService sInstance =
+                StubMediaRoute2ProviderService.getInstance();
+        assertNotNull(sInstance);
+        sInstance.removeRoute(ROUTE_ID2);
+
+        // Wait until the route is removed.
+        assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        Map<String, MediaRoute2Info> newRoutes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        assertNull(newRoutes.get(ROUTE_ID2));
+
+        // Revert the removal.
+        sInstance.addRoute(routeToRemove);
+        assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        mRouter2.unregisterRouteCallback(routeCallback);
+    }
+
     /**
      * Tests if we get proper routes for application that has special route feature.
      */
@@ -465,6 +516,56 @@
         assertEquals(VOLUME_MAX, variableVolumeRoute.getVolumeMax());
     }
 
+    @Test
+    public void testRouter2SetOnGetControllerHintsListener() throws Exception {
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL);
+        addRouterCallback(new RouteCallback() {});
+
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final Bundle controllerHints = new Bundle();
+        controllerHints.putString(TEST_KEY, TEST_VALUE);
+        final CountDownLatch hintLatch = new CountDownLatch(1);
+        final MediaRouter2.OnGetControllerHintsListener listener =
+                route1 -> {
+                    hintLatch.countDown();
+                    return controllerHints;
+                };
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+
+        addManagerCallback(new MediaRouter2Manager.Callback() {
+            @Override
+            public void onTransferred(RoutingSessionInfo oldSession,
+                    RoutingSessionInfo newSession) {
+                assertTrue(newSession.getSelectedRoutes().contains(route.getId()));
+                // The StubMediaRoute2ProviderService is supposed to set control hints
+                // with the given controllerHints.
+                Bundle controlHints = newSession.getControlHints();
+                assertNotNull(controlHints);
+                assertTrue(controlHints.containsKey(TEST_KEY));
+                assertEquals(TEST_VALUE, controlHints.getString(TEST_KEY));
+
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onTransferFailed(RoutingSessionInfo session,
+                    MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+        });
+
+        mRouter2.setOnGetControllerHintsListener(listener);
+        mManager.selectRoute(mPackageName, route);
+        assertTrue(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+        assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
     Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> routeFeatures)
             throws Exception {
         CountDownLatch addedLatch = new CountDownLatch(1);
@@ -475,8 +576,8 @@
         MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() {
             @Override
             public void onRoutesAdded(List<MediaRoute2Info> routes) {
-                for (int i = 0; i < routes.size(); i++) {
-                    if (!routes.get(i).isSystemRoute()) {
+                for (MediaRoute2Info route : routes) {
+                    if (!route.isSystemRoute()) {
                         addedLatch.countDown();
                         break;
                     }
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
index 6d46ba5..4e398f2 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java
@@ -65,9 +65,9 @@
     public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route";
 
     public static final String FEATURE_SAMPLE =
-            "com.android.mediarouteprovider.FEATURE_SAMPLE";
+            "com.android.mediaroutertest.FEATURE_SAMPLE";
     public static final String FEATURE_SPECIAL =
-            "com.android.mediarouteprovider.FEATURE_SPECIAL";
+            "com.android.mediaroutertest..FEATURE_SPECIAL";
 
     Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
     Map<String, String> mRouteIdToSessionId = new HashMap<>();
diff --git a/packages/CarSystemUI/res/layout/car_qs_footer.xml b/packages/CarSystemUI/res/layout/car_qs_footer.xml
deleted file mode 100644
index bf96c00..0000000
--- a/packages/CarSystemUI/res/layout/car_qs_footer.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 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.
--->
-<!-- extends RelativeLayout -->
-<com.android.systemui.qs.car.CarQSFooter
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/qs_footer"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/car_qs_footer_height"
-    android:baselineAligned="false"
-    android:clickable="false"
-    android:clipChildren="false"
-    android:clipToPadding="false"
-    android:paddingBottom="@dimen/car_qs_footer_padding_bottom"
-    android:paddingTop="@dimen/car_qs_footer_padding_top"
-    android:paddingEnd="@dimen/car_qs_footer_padding_end"
-    android:paddingStart="@dimen/car_qs_footer_padding_start"
-    android:gravity="center_vertical">
-
-    <com.android.systemui.statusbar.phone.MultiUserSwitch
-        android:id="@+id/multi_user_switch"
-        android:layout_alignParentStart="true"
-        android:layout_centerVertical="true"
-        android:layout_width="@dimen/car_qs_footer_icon_width"
-        android:layout_height="@dimen/car_qs_footer_icon_height"
-        android:background="?android:attr/selectableItemBackground"
-        android:focusable="true">
-
-        <ImageView
-            android:id="@+id/multi_user_avatar"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:scaleType="fitCenter"/>
-    </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
-    <ImageView
-        android:id="@+id/user_switch_expand_icon"
-        android:layout_height="match_parent"
-        android:layout_width="@dimen/car_qs_footer_user_switch_icon_width"
-        android:layout_centerVertical="true"
-        android:layout_toEndOf="@+id/multi_user_switch"
-        android:layout_marginLeft="@dimen/car_qs_footer_user_switch_icon_margin"
-        android:layout_marginRight="@dimen/car_qs_footer_user_switch_icon_margin"
-        android:src="@drawable/car_ic_arrow_drop_up"
-        android:scaleType="fitCenter">
-    </ImageView>
-
-    <TextView android:id="@+id/user_name"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textSize="@dimen/car_qs_footer_user_name_text_size"
-        android:textColor="@color/car_qs_footer_user_name_color"
-        android:gravity="start|center_vertical"
-        android:layout_centerVertical="true"
-        android:layout_toEndOf="@id/user_switch_expand_icon" />
-
-    <com.android.systemui.statusbar.phone.SettingsButton
-        android:id="@+id/settings_button"
-        android:layout_alignParentEnd="true"
-        android:layout_centerVertical="true"
-        android:layout_width="@dimen/car_qs_footer_icon_width"
-        android:layout_height="@dimen/car_qs_footer_icon_height"
-        android:background="@drawable/ripple_drawable"
-        android:contentDescription="@string/accessibility_quick_settings_settings"
-        android:scaleType="centerCrop"
-        android:src="@drawable/ic_settings_16dp"
-        android:tint="?android:attr/colorForeground"
-        style="@android:style/Widget.Material.Button.Borderless" />
-
-</com.android.systemui.qs.car.CarQSFooter>
diff --git a/packages/CarSystemUI/res/layout/car_qs_panel.xml b/packages/CarSystemUI/res/layout/car_qs_panel.xml
deleted file mode 100644
index 0c6f322..0000000
--- a/packages/CarSystemUI/res/layout/car_qs_panel.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/quick_settings_container"
-    android:clipChildren="false"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="@color/car_qs_background_primary"
-    android:orientation="vertical"
-    android:elevation="4dp">
-
-    <include layout="@layout/car_status_bar_header"/>
-    <include layout="@layout/car_qs_footer"/>
-
-    <RelativeLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/user_switcher_container"
-        android:clipChildren="false"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/car_user_switcher_container_height">
-
-        <com.android.systemui.car.userswitcher.UserGridRecyclerView
-            android:id="@+id/user_grid"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-
-    </RelativeLayout>
-
-</LinearLayout>
diff --git a/packages/CarSystemUI/res/layout/car_status_bar_header.xml b/packages/CarSystemUI/res/layout/car_status_bar_header.xml
index 81c7108..12c9f11 100644
--- a/packages/CarSystemUI/res/layout/car_status_bar_header.xml
+++ b/packages/CarSystemUI/res/layout/car_status_bar_header.xml
@@ -15,7 +15,7 @@
   ~ limitations under the License
   -->
 <!-- Extends LinearLayout -->
-<com.android.systemui.qs.car.CarStatusBarHeader
+<com.android.systemui.car.userswitcher.CarStatusBarHeader
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/header"
     android:layout_width="match_parent"
@@ -27,4 +27,4 @@
         android:layout_height="match_parent"
         android:layout_weight="1"
     />
-</com.android.systemui.qs.car.CarStatusBarHeader>
+</com.android.systemui.car.userswitcher.CarStatusBarHeader>
diff --git a/packages/CarSystemUI/res/values/colors.xml b/packages/CarSystemUI/res/values/colors.xml
index 7972e09..3e44721 100644
--- a/packages/CarSystemUI/res/values/colors.xml
+++ b/packages/CarSystemUI/res/values/colors.xml
@@ -20,6 +20,7 @@
     <color name="car_user_switcher_background_color">#000000</color>
     <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color>
     <color name="car_user_switcher_add_user_background_color">#131313</color>
+    <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color>
     <color name="car_nav_icon_fill_color">#8Fffffff</color>
     <color name="car_nav_icon_fill_color_selected">#ffffff</color>
     <!-- colors for seekbar -->
diff --git a/packages/CarSystemUI/res/values/colors_car.xml b/packages/CarSystemUI/res/values/colors_car.xml
deleted file mode 100644
index 5f33f8f..0000000
--- a/packages/CarSystemUI/res/values/colors_car.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright 2018, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-<resources>
-    <color name="car_qs_background_primary">#263238</color> <!-- Blue Gray 900 -->
-    <color name="car_qs_footer_user_name_color">@*android:color/car_grey_50</color>
-
-    <!-- colors for user switcher -->
-    <color name="car_user_switcher_background_color">@*android:color/car_card_dark</color>
-    <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color>
-    <color name="car_user_switcher_add_user_background_color">@*android:color/car_dark_blue_grey_600</color>
-    <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color>
-</resources>
diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml
index f68d034..9014eb1 100644
--- a/packages/CarSystemUI/res/values/dimens.xml
+++ b/packages/CarSystemUI/res/values/dimens.xml
@@ -22,16 +22,11 @@
 
     <dimen name="status_bar_icon_drawing_size_dark">36dp</dimen>
     <dimen name="status_bar_icon_drawing_size">36dp</dimen>
-    <dimen name="car_qs_header_system_icons_area_height">96dp</dimen>
     <!-- The amount by which to scale up the status bar icons. -->
     <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.75</item>
 
     <dimen name="car_primary_icon_size">@*android:dimen/car_primary_icon_size</dimen>
 
-    <!-- dimensions for the car user switcher -->
-    <dimen name="car_user_switcher_name_text_size">@dimen/car_body1_size</dimen>
-    <dimen name="car_user_switcher_vertical_spacing_between_users">124dp</dimen>
-
     <!--These values represent MIN and MAX for hvac-->
     <item name="hvac_min_value" format="float" type="dimen">0</item>
     <item name="hvac_max_value" format="float" type="dimen">126</item>
@@ -90,9 +85,6 @@
     <!-- The width of panel holding the notification card. -->
     <dimen name="notification_panel_width">522dp</dimen>
 
-    <!-- The width of the quick settings panel. -1 for match_parent. -->
-    <dimen name="qs_panel_width">-1px</dimen>
-
     <!-- Height of a small notification in the status bar-->
     <dimen name="notification_min_height">192dp</dimen>
 
@@ -149,7 +141,19 @@
          child closer so there is less wasted space. -->
     <dimen name="notification_children_container_margin_top">68dp</dimen>
 
-    <!-- The height of the quick settings footer that holds the user switcher, settings icon,
-         etc. in the car setting.-->
-    <dimen name="qs_footer_height">74dp</dimen>
+    <!-- dimensions for the car user switcher -->
+    <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen>
+    <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen>
+    <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen>
+    <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen>
+    <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen>
+
+    <dimen name="car_navigation_button_width">64dp</dimen>
+    <dimen name="car_navigation_bar_width">760dp</dimen>
+    <dimen name="car_left_navigation_bar_width">96dp</dimen>
+    <dimen name="car_right_navigation_bar_width">96dp</dimen>
+
+    <dimen name="car_user_switcher_container_height">420dp</dimen>
+    <!-- This must be the negative of car_user_switcher_container_height for the animation. -->
+    <dimen name="car_user_switcher_container_anim_height">-420dp</dimen>
 </resources>
diff --git a/packages/CarSystemUI/res/values/dimens_car.xml b/packages/CarSystemUI/res/values/dimens_car.xml
deleted file mode 100644
index e7ecf7f..0000000
--- a/packages/CarSystemUI/res/values/dimens_car.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright (c) 2018, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
-*/
--->
-<resources>
-    <!-- dimensions for the car user switcher -->
-    <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen>
-    <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen>
-    <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen>
-    <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen>
-    <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen>
-
-    <dimen name="car_navigation_button_width">64dp</dimen>
-    <dimen name="car_navigation_bar_width">760dp</dimen>
-    <dimen name="car_left_navigation_bar_width">96dp</dimen>
-    <dimen name="car_right_navigation_bar_width">96dp</dimen>
-
-    <dimen name="car_qs_footer_height">112dp</dimen>
-    <dimen name="car_qs_footer_padding_bottom">16dp</dimen>
-    <dimen name="car_qs_footer_padding_top">16dp</dimen>
-    <dimen name="car_qs_footer_padding_end">46dp</dimen>
-    <dimen name="car_qs_footer_padding_start">46dp</dimen>
-    <dimen name="car_qs_footer_icon_width">56dp</dimen>
-    <dimen name="car_qs_footer_icon_height">56dp</dimen>
-    <dimen name="car_qs_footer_user_switch_icon_margin">5dp</dimen>
-    <dimen name="car_qs_footer_user_switch_icon_width">36dp</dimen>
-    <dimen name="car_qs_footer_user_name_text_size">@*android:dimen/car_body2_size</dimen>
-
-    <dimen name="car_user_switcher_container_height">420dp</dimen>
-    <!-- This must be the negative of car_user_switcher_container_height for the animation. -->
-    <dimen name="car_user_switcher_container_anim_height">-420dp</dimen>
-</resources>
diff --git a/packages/CarSystemUI/res/values/ids_car.xml b/packages/CarSystemUI/res/values/ids.xml
similarity index 100%
rename from packages/CarSystemUI/res/values/ids_car.xml
rename to packages/CarSystemUI/res/values/ids.xml
diff --git a/packages/CarSystemUI/res/values/integers.xml b/packages/CarSystemUI/res/values/integers.xml
index 8b87c74..5ae5555 100644
--- a/packages/CarSystemUI/res/values/integers.xml
+++ b/packages/CarSystemUI/res/values/integers.xml
@@ -16,5 +16,19 @@
 -->
 
 <resources>
-    <integer name="user_fullscreen_switcher_num_col">2</integer>
+    <!-- Full screen user switcher column number -->
+    <integer name="user_fullscreen_switcher_num_col">3</integer>
+
+    <!--Percentage of the screen height, from the bottom, that a notification panel being
+    partially closed at will result in it remaining open if released-->
+    <integer name="notification_settle_open_percentage">20</integer>
+    <!--Percentage of the screen height, from the bottom, that a notification panel being peeked
+    at will result in remaining closed the panel if released-->
+    <integer name="notification_settle_close_percentage">80</integer>
+
+    <!-- Timeout values in milliseconds for displaying volume dialog-->
+    <integer name="car_volume_dialog_display_normal_timeout">3000</integer>
+    <integer name="car_volume_dialog_display_hovering_timeout">16000</integer>
+    <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer>
+    <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer>
 </resources>
diff --git a/packages/CarSystemUI/res/values/integers_car.xml b/packages/CarSystemUI/res/values/integers_car.xml
deleted file mode 100644
index db8ce954..0000000
--- a/packages/CarSystemUI/res/values/integers_car.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (c) 2018, The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <!-- Full screen user switcher column number TODO: move to support library-->
-    <integer name="user_fullscreen_switcher_num_col">3</integer>
-
-    <!--Percentage of the screen height, from the bottom, that a notification panel being
-    partially closed at will result in it remaining open if released-->
-    <integer name="notification_settle_open_percentage">20</integer>
-    <!--Percentage of the screen height, from the bottom, that a notification panel being peeked
-    at will result in remaining closed the panel if released-->
-    <integer name="notification_settle_close_percentage">80</integer>
-
-    <!-- Timeout values in milliseconds for displaying volume dialog-->
-    <integer name="car_volume_dialog_display_normal_timeout">3000</integer>
-    <integer name="car_volume_dialog_display_hovering_timeout">16000</integer>
-    <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer>
-    <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer>
-</resources>
diff --git a/packages/CarSystemUI/res/values/strings.xml b/packages/CarSystemUI/res/values/strings.xml
index 9ea7ed0..881e746 100644
--- a/packages/CarSystemUI/res/values/strings.xml
+++ b/packages/CarSystemUI/res/values/strings.xml
@@ -22,4 +22,16 @@
     <string name="hvac_max_text">Max</string>
     <!-- Text for voice recognition toast. [CHAR LIMIT=60] -->
     <string name="voice_recognition_toast">Voice recognition now handled by connected Bluetooth device</string>
+    <!-- Name of Guest Profile. [CHAR LIMIT=30] -->
+    <string name="car_guest">Guest</string>
+    <!-- Title for button that starts a guest session. [CHAR LIMIT=30] -->
+    <string name="start_guest_session">Guest</string>
+    <!-- Title for button that  adds a new user. [CHAR LIMIT=30] -->
+    <string name="car_add_user">Add User</string>
+    <!-- Default name of the new user created. [CHAR LIMIT=30] -->
+    <string name="car_new_user">New User</string>
+    <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] -->
+    <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string>
+    <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] -->
+    <string name="user_add_user_message_update">Any user can update apps for all other users.</string>
 </resources>
diff --git a/packages/CarSystemUI/res/values/strings_car.xml b/packages/CarSystemUI/res/values/strings_car.xml
deleted file mode 100644
index 83e91c5..0000000
--- a/packages/CarSystemUI/res/values/strings_car.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/**
- * Copyright (c) 2018, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-<resources>
-    <!-- Name of Guest Profile. [CHAR LIMIT=30] -->
-    <string name="car_guest">Guest</string>
-    <!-- Title for button that starts a guest session. [CHAR LIMIT=30] -->
-    <string name="start_guest_session">Guest</string>
-    <!-- Title for button that  adds a new user. [CHAR LIMIT=30] -->
-    <string name="car_add_user">Add User</string>
-    <!-- Default name of the new user created. [CHAR LIMIT=30] -->
-    <string name="car_new_user">New User</string>
-    <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] -->
-    <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string>
-    <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] -->
-    <string name="user_add_user_message_update">Any user can update apps for all other users.</string>
-</resources>
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java
similarity index 95%
rename from packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java
rename to packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java
index 4ef926f..bab6715 100644
--- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.car;
+package com.android.systemui.car.userswitcher;
 
 import android.content.Context;
 import android.graphics.Color;
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
index 58add17..2ff6670 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java
@@ -368,7 +368,7 @@
 
         private void applyCarSysUIDialogFlags(AlertDialog dialog) {
             final Window window = dialog.getWindow();
-            window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+            window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
             window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
                     | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
             window.getAttributes().setFitInsetsTypes(
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java
deleted file mode 100644
index b74f199..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2018 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.qs.car;
-
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.qs.QSFooter;
-import com.android.systemui.qs.QSPanel;
-import com.android.systemui.statusbar.phone.MultiUserSwitch;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.UserInfoController;
-
-/**
- * The footer view that displays below the status bar in the auto use-case. This view shows the
- * user switcher and access to settings.
- */
-public class CarQSFooter extends RelativeLayout implements QSFooter,
-        UserInfoController.OnUserInfoChangedListener {
-    private static final String TAG = "CarQSFooter";
-
-    private UserInfoController mUserInfoController;
-
-    private MultiUserSwitch mMultiUserSwitch;
-    private TextView mUserName;
-    private ImageView mMultiUserAvatar;
-    private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
-
-    public CarQSFooter(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mMultiUserSwitch = findViewById(R.id.multi_user_switch);
-        mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
-        mUserName = findViewById(R.id.user_name);
-
-        mUserInfoController = Dependency.get(UserInfoController.class);
-
-        mMultiUserSwitch.setOnClickListener(v -> {
-            if (mUserSwitchCallback == null) {
-                Log.e(TAG, "CarQSFooter not properly set up; cannot display user switcher.");
-                return;
-            }
-
-            if (!mUserSwitchCallback.isShowing()) {
-                mUserSwitchCallback.show();
-            } else {
-                mUserSwitchCallback.hide();
-            }
-        });
-
-        findViewById(R.id.settings_button).setOnClickListener(v -> {
-            ActivityStarter activityStarter = Dependency.get(ActivityStarter.class);
-
-            if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) {
-                // If user isn't setup just unlock the device and dump them back at SUW.
-                activityStarter.postQSRunnableDismissingKeyguard(() -> { });
-                return;
-            }
-
-            activityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS),
-                    true /* dismissShade */);
-        });
-    }
-
-    @Override
-    public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
-        mMultiUserAvatar.setImageDrawable(picture);
-        mUserName.setText(name);
-    }
-
-    @Override
-    public void setQSPanel(@Nullable QSPanel panel) {
-        if (panel != null) {
-            mMultiUserSwitch.setQsPanel(panel);
-        }
-    }
-
-    public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
-        mUserSwitchCallback = callback;
-    }
-
-    @Override
-    public void setListening(boolean listening) {
-        if (listening) {
-            mUserInfoController.addCallback(this);
-        } else {
-            mUserInfoController.removeCallback(this);
-        }
-    }
-
-    @Override
-    public void setExpandClickListener(OnClickListener onClickListener) {
-        // No view that should expand/collapse the quick settings.
-    }
-
-    @Override
-    public void setExpanded(boolean expanded) {
-        // Do nothing because the quick settings cannot be expanded.
-    }
-
-    @Override
-    public void setExpansion(float expansion) {
-        // Do nothing because the quick settings cannot be expanded.
-    }
-
-    @Override
-    public void setKeyguardShowing(boolean keyguardShowing) {
-        // Do nothing because the footer will not be shown when the keyguard is up.
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java
deleted file mode 100644
index 31965c5..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright (C) 2018 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.qs.car;
-
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.app.Fragment;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.recyclerview.widget.GridLayoutManager;
-
-import com.android.systemui.R;
-import com.android.systemui.car.userswitcher.UserGridRecyclerView;
-import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.QSFooter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A quick settings fragment for the car. For auto, there is no row for quick settings or ability
- * to expand the quick settings panel. Instead, the only thing is that displayed is the
- * status bar, and a static row with access to the user switcher and settings.
- */
-public class CarQSFragment extends Fragment implements QS {
-    private View mHeader;
-    private View mUserSwitcherContainer;
-    private CarQSFooter mFooter;
-    private View mFooterUserName;
-    private View mFooterExpandIcon;
-    private UserGridRecyclerView mUserGridView;
-    private AnimatorSet mAnimatorSet;
-    private UserSwitchCallback mUserSwitchCallback;
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-            Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.car_qs_panel, container, false);
-    }
-
-    @Override
-    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        mHeader = view.findViewById(R.id.header);
-        mFooter = view.findViewById(R.id.qs_footer);
-        mFooterUserName = mFooter.findViewById(R.id.user_name);
-        mFooterExpandIcon = mFooter.findViewById(R.id.user_switch_expand_icon);
-
-        mUserSwitcherContainer = view.findViewById(R.id.user_switcher_container);
-
-        updateUserSwitcherHeight(0);
-
-        Context context = getContext();
-        mUserGridView = mUserSwitcherContainer.findViewById(R.id.user_grid);
-        GridLayoutManager layoutManager = new GridLayoutManager(context,
-                context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
-        mUserGridView.setLayoutManager(layoutManager);
-        mUserGridView.buildAdapter();
-
-        mUserSwitchCallback = new UserSwitchCallback();
-        mFooter.setUserSwitchCallback(mUserSwitchCallback);
-    }
-
-    @Override
-    public void hideImmediately() {
-        getView().setVisibility(View.INVISIBLE);
-    }
-
-    @Override
-    public void setQsExpansion(float qsExpansionFraction, float headerTranslation) {
-        // If the header is to be completed translated down, then set it to be visible.
-        getView().setVisibility(headerTranslation == 0 ? View.VISIBLE : View.INVISIBLE);
-    }
-
-    @Override
-    public View getHeader() {
-        return mHeader;
-    }
-
-    @VisibleForTesting
-    QSFooter getFooter() {
-        return mFooter;
-    }
-
-    @Override
-    public void setHeaderListening(boolean listening) {
-        mFooter.setListening(listening);
-    }
-
-    @Override
-    public void setListening(boolean listening) {
-        mFooter.setListening(listening);
-    }
-
-    @Override
-    public int getQsMinExpansionHeight() {
-        return getView().getHeight();
-    }
-
-    @Override
-    public int getDesiredHeight() {
-        return getView().getHeight();
-    }
-
-    @Override
-    public void setPanelView(HeightListener notificationPanelView) {
-        // No quick settings panel.
-    }
-
-    @Override
-    public void setHeightOverride(int desiredHeight) {
-        // No ability to expand quick settings.
-    }
-
-    @Override
-    public void setHeaderClickable(boolean qsExpansionEnabled) {
-        // Usually this sets the expand button to be clickable, but there is no quick settings to
-        // expand.
-    }
-
-    @Override
-    public boolean isCustomizing() {
-        // No ability to customize the quick settings.
-        return false;
-    }
-
-    @Override
-    public void setOverscrolling(boolean overscrolling) {
-        // No overscrolling to reveal quick settings.
-    }
-
-    @Override
-    public void setExpanded(boolean qsExpanded) {
-        // No quick settings to expand
-    }
-
-    @Override
-    public boolean isShowingDetail() {
-        // No detail panel to close.
-        return false;
-    }
-
-    @Override
-    public void closeDetail() {
-        // No detail panel to close.
-    }
-
-    @Override
-    public void animateHeaderSlidingIn(long delay) {
-        // No header to animate.
-    }
-
-    @Override
-    public void animateHeaderSlidingOut() {
-        // No header to animate.
-    }
-
-    @Override
-    public void notifyCustomizeChanged() {
-        // There is no ability to customize quick settings.
-    }
-
-    @Override
-    public void setContainer(ViewGroup container) {
-        // No quick settings, so no container to set.
-    }
-
-    @Override
-    public void setExpandClickListener(OnClickListener onClickListener) {
-        // No ability to expand the quick settings.
-    }
-
-    public class UserSwitchCallback {
-        private boolean mShowing;
-
-        public boolean isShowing() {
-            return mShowing;
-        }
-
-        public void show() {
-            mShowing = true;
-            animateHeightChange(true /* opening */);
-        }
-
-        public void hide() {
-            mShowing = false;
-            animateHeightChange(false /* opening */);
-        }
-    }
-
-    private void updateUserSwitcherHeight(int height) {
-        ViewGroup.LayoutParams layoutParams = mUserSwitcherContainer.getLayoutParams();
-        layoutParams.height = height;
-        mUserSwitcherContainer.requestLayout();
-    }
-
-    private void animateHeightChange(boolean opening) {
-        // Animation in progress; cancel it to avoid contention.
-        if (mAnimatorSet != null) {
-            mAnimatorSet.cancel();
-        }
-
-        List<Animator> allAnimators = new ArrayList<>();
-        ValueAnimator heightAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(),
-                opening ? R.anim.car_user_switcher_open_animation
-                        : R.anim.car_user_switcher_close_animation);
-        heightAnimator.addUpdateListener(valueAnimator -> {
-            updateUserSwitcherHeight((Integer) valueAnimator.getAnimatedValue());
-        });
-        allAnimators.add(heightAnimator);
-
-        Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(),
-                opening ? R.anim.car_user_switcher_open_name_animation
-                        : R.anim.car_user_switcher_close_name_animation);
-        nameAnimator.setTarget(mFooterUserName);
-        allAnimators.add(nameAnimator);
-
-        Animator iconAnimator = AnimatorInflater.loadAnimator(getContext(),
-                opening ? R.anim.car_user_switcher_open_icon_animation
-                        : R.anim.car_user_switcher_close_icon_animation);
-        iconAnimator.setTarget(mFooterExpandIcon);
-        allAnimators.add(iconAnimator);
-
-        mAnimatorSet = new AnimatorSet();
-        mAnimatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimatorSet = null;
-            }
-        });
-        mAnimatorSet.playTogether(allAnimators.toArray(new Animator[0]));
-
-        // Setup all values to the start values in the animations, since there are delays, but need
-        // to have all values start at the beginning.
-        setupInitialValues(mAnimatorSet);
-
-        mAnimatorSet.start();
-    }
-
-    private void setupInitialValues(Animator anim) {
-        if (anim instanceof AnimatorSet) {
-            for (Animator a : ((AnimatorSet) anim).getChildAnimations()) {
-                setupInitialValues(a);
-            }
-        } else if (anim instanceof ObjectAnimator) {
-            ((ObjectAnimator) anim).setCurrentFraction(0.0f);
-        }
-    }
-}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index d8111d0..ec1dabc 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -54,7 +54,6 @@
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.PluginDependencyProvider;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qs.car.CarQSFragment;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.shared.plugins.PluginManager;
@@ -407,7 +406,7 @@
 
     @Override
     protected QS createDefaultQSFragment() {
-        return new CarQSFragment();
+        return null;
     }
 
     private BatteryController createBatteryController() {
diff --git a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
index 0dbe1a3..04d69ea 100644
--- a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java
@@ -90,7 +90,7 @@
         mLp = new WindowManager.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT,
-                WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL,
+                WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE,
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                         | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING
                         | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index 587109d..54986a4 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -69,6 +69,14 @@
     // Explicitly uncompress native libs rather than letting the build system doing it and destroy the
     // v2/v3 signature.
     use_embedded_native_libs: true,
+    apex_available: [
+        "com.android.apex.cts.shim.v1",
+        "com.android.apex.cts.shim.v2",
+        "com.android.apex.cts.shim.v2_no_hashtree",
+        "com.android.apex.cts.shim.v2_legacy",
+        "com.android.apex.cts.shim.v2_sdk_target_p",
+        "com.android.apex.cts.shim.v3",
+    ],
 }
 
 //##########################################################
@@ -110,7 +118,10 @@
     dex_preopt: {
         enabled: false,
     },
-    manifest: "shim/AndroidManifestTargetPSdk.xml"
+    manifest: "shim/AndroidManifestTargetPSdk.xml",
+    apex_available: [
+        "com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p",
+    ],
 }
 
 //##########################################################
@@ -128,4 +139,12 @@
     },
 
     manifest: "shim/AndroidManifest.xml",
+    apex_available: [
+        "com.android.apex.cts.shim.v1",
+        "com.android.apex.cts.shim.v2",
+        "com.android.apex.cts.shim.v2_no_hashtree",
+        "com.android.apex.cts.shim.v2_legacy",
+        "com.android.apex.cts.shim.v2_sdk_target_p",
+        "com.android.apex.cts.shim.v3",
+    ],
 }
diff --git a/packages/CtsShim/build/jni/Android.bp b/packages/CtsShim/build/jni/Android.bp
index ea15b43..7a5b07e 100644
--- a/packages/CtsShim/build/jni/Android.bp
+++ b/packages/CtsShim/build/jni/Android.bp
@@ -18,4 +18,13 @@
     name: "libshim_jni",
     srcs: ["Shim.c"],
     sdk_version: "24",
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.apex.cts.shim.v1",
+        "com.android.apex.cts.shim.v2",
+        "com.android.apex.cts.shim.v2_no_hashtree",
+        "com.android.apex.cts.shim.v2_legacy",
+        "com.android.apex.cts.shim.v2_sdk_target_p",
+        "com.android.apex.cts.shim.v3",
+    ],
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index e551b69..ee8fb38 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -38,7 +38,7 @@
 
     BluetoothMediaDevice(Context context, CachedBluetoothDevice device,
             MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) {
-        super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
         mCachedDevice = device;
         initDeviceRecord();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
index 85fa988..83a9671 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
@@ -38,7 +38,7 @@
 
     InfoMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
             String packageName) {
-        super(context, MediaDeviceType.TYPE_CAST_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
         initDeviceRecord();
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 39e6a12..6aff301 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -15,6 +15,16 @@
  */
 package com.android.settingslib.media;
 
+import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_GROUP;
+import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
+import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
@@ -38,13 +48,21 @@
     private static final String TAG = "MediaDevice";
 
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({MediaDeviceType.TYPE_CAST_DEVICE,
+    @IntDef({MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE,
+            MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE,
+            MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE,
             MediaDeviceType.TYPE_BLUETOOTH_DEVICE,
+            MediaDeviceType.TYPE_CAST_DEVICE,
+            MediaDeviceType.TYPE_CAST_GROUP_DEVICE,
             MediaDeviceType.TYPE_PHONE_DEVICE})
     public @interface MediaDeviceType {
-        int TYPE_PHONE_DEVICE = 1;
-        int TYPE_CAST_DEVICE = 2;
-        int TYPE_BLUETOOTH_DEVICE = 3;
+        int TYPE_USB_C_AUDIO_DEVICE = 1;
+        int TYPE_3POINT5_MM_AUDIO_DEVICE = 2;
+        int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 3;
+        int TYPE_BLUETOOTH_DEVICE = 4;
+        int TYPE_CAST_DEVICE = 5;
+        int TYPE_CAST_GROUP_DEVICE = 6;
+        int TYPE_PHONE_DEVICE = 7;
     }
 
     @VisibleForTesting
@@ -58,13 +76,43 @@
     protected final MediaRouter2Manager mRouterManager;
     protected final String mPackageName;
 
-    MediaDevice(Context context, @MediaDeviceType int type, MediaRouter2Manager routerManager,
-            MediaRoute2Info info, String packageName) {
-        mType = type;
+    MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
+            String packageName) {
         mContext = context;
         mRouteInfo = info;
         mRouterManager = routerManager;
         mPackageName = packageName;
+        setType(info);
+    }
+
+    private void setType(MediaRoute2Info info) {
+        if (info == null) {
+            mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
+            return;
+        }
+
+        switch (info.getType()) {
+            case TYPE_GROUP:
+                mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE;
+                break;
+            case TYPE_BUILTIN_SPEAKER:
+                mType = MediaDeviceType.TYPE_PHONE_DEVICE;
+                break;
+            case TYPE_WIRED_HEADSET:
+            case TYPE_WIRED_HEADPHONES:
+                mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE;
+                break;
+            case TYPE_HEARING_AID:
+            case TYPE_BLUETOOTH_A2DP:
+                mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
+                break;
+            case TYPE_UNKNOWN:
+            case TYPE_REMOTE_TV:
+            case TYPE_REMOTE_SPEAKER:
+            default:
+                mType = MediaDeviceType.TYPE_CAST_DEVICE;
+                break;
+        }
     }
 
     void initDeviceRecord() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index af88723..c6c5ade 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -42,7 +42,7 @@
 
     PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info,
             String packageName) {
-        super(context, MediaDeviceType.TYPE_PHONE_DEVICE, routerManager, info, packageName);
+        super(context, routerManager, info, packageName);
 
         initDeviceRecord();
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index c713d78..d7e76a1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -133,6 +133,35 @@
         }
     }
 
+    /**
+     * Fetches initial state as if a WifiManager.NETWORK_STATE_CHANGED_ACTION have been received.
+     * This replaces the dependency on the initial sticky broadcast.
+     */
+    public void fetchInitialState() {
+        if (mWifiManager == null) {
+            return;
+        }
+        updateWifiState();
+        final NetworkInfo networkInfo =
+                mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+        connected = networkInfo != null && networkInfo.isConnected();
+        mWifiInfo = null;
+        ssid = null;
+        if (connected) {
+            mWifiInfo = mWifiManager.getConnectionInfo();
+            if (mWifiInfo != null) {
+                if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) {
+                    ssid = mWifiInfo.getPasspointProviderFriendlyName();
+                } else {
+                    ssid = getValidSsid(mWifiInfo);
+                }
+                updateRssi(mWifiInfo.getRssi());
+                maybeRequestNetworkScore();
+            }
+        }
+        updateStatusLabel();
+    }
+
     public void handleBroadcast(Intent intent) {
         if (mWifiManager == null) {
             return;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 4b08387..db05b76 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -15,6 +15,10 @@
  */
 package com.android.settingslib.media;
 
+import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.verify;
@@ -144,12 +148,19 @@
         when(mCachedDevice2.isConnected()).thenReturn(true);
         when(mCachedDevice3.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
         when(mCachedDevice3.isConnected()).thenReturn(true);
+        when(mBluetoothRouteInfo1.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
+        when(mBluetoothRouteInfo2.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
+        when(mBluetoothRouteInfo3.getType()).thenReturn(TYPE_BLUETOOTH_A2DP);
         when(mRouteInfo1.getId()).thenReturn(ROUTER_ID_1);
         when(mRouteInfo2.getId()).thenReturn(ROUTER_ID_2);
         when(mRouteInfo3.getId()).thenReturn(ROUTER_ID_3);
         when(mRouteInfo1.getName()).thenReturn(DEVICE_NAME_1);
         when(mRouteInfo2.getName()).thenReturn(DEVICE_NAME_2);
         when(mRouteInfo3.getName()).thenReturn(DEVICE_NAME_3);
+        when(mRouteInfo1.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mRouteInfo2.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mRouteInfo3.getType()).thenReturn(TYPE_REMOTE_SPEAKER);
+        when(mPhoneRouteInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
         when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
         when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
         when(mProfileManager.getHearingAidProfile()).thenReturn(mHapProfile);
@@ -271,12 +282,12 @@
 
     @Test
     public void compareTo_info_bluetooth_infoFirst() {
-        mMediaDevices.add(mBluetoothMediaDevice1);
         mMediaDevices.add(mInfoMediaDevice1);
+        mMediaDevices.add(mBluetoothMediaDevice1);
 
-        assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1);
-        Collections.sort(mMediaDevices, COMPARATOR);
         assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1);
+        Collections.sort(mMediaDevices, COMPARATOR);
+        assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1);
     }
 
     @Test
@@ -327,7 +338,7 @@
     // 5.mBluetoothMediaDevice2: * 2 times usage
     // 6.mBluetoothMediaDevice3: * 1 time usage
     // 7.mPhoneMediaDevice:      * 0 time usage
-    // Order: 7 -> 2 -> 1 -> 3 -> 5 -> 4 -> 6
+    // Order: 7 -> 2 -> 1 -> 5 -> 3 -> 6 -> 4
     @Test
     public void compareTo_mixedDevices_carKitFirst() {
         when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass);
@@ -352,10 +363,10 @@
         assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice);
         assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice1);
         assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice1);
-        assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice2);
-        assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice2);
-        assertThat(mMediaDevices.get(5)).isEqualTo(mInfoMediaDevice3);
-        assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice2);
+        assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice2);
+        assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(6)).isEqualTo(mInfoMediaDevice3);
     }
 
     // 1.mInfoMediaDevice1:      Last Selected device
@@ -365,7 +376,7 @@
     // 5.mBluetoothMediaDevice2: * 4 times usage not connected
     // 6.mBluetoothMediaDevice3: * 1 time usage
     // 7.mPhoneMediaDevice:      * 0 time usage
-    // Order: 7 -> 1 -> 3 -> 4 -> 6 -> 2 -> 5
+    // Order: 7 -> 1 -> 3 -> 6 -> 4  -> 2 -> 5
     @Test
     public void compareTo_mixedDevices_connectDeviceFirst() {
         when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass);
@@ -394,8 +405,8 @@
         assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice);
         assertThat(mMediaDevices.get(1)).isEqualTo(mInfoMediaDevice1);
         assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice2);
-        assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice3);
-        assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice3);
+        assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice3);
         assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice1);
         assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice2);
     }
diff --git a/packages/SystemUI/res/layout/global_actions_grid_v2.xml b/packages/SystemUI/res/layout/global_actions_grid_v2.xml
index dba003a..1c4ec64 100644
--- a/packages/SystemUI/res/layout/global_actions_grid_v2.xml
+++ b/packages/SystemUI/res/layout/global_actions_grid_v2.xml
@@ -15,8 +15,7 @@
       android:clipChildren="false"
       android:clipToPadding="false"
       android:layout_marginTop="@dimen/global_actions_top_margin"
-      android:layout_marginLeft="@dimen/global_actions_side_margin"
-      android:layout_marginRight="@dimen/global_actions_side_margin"
+      android:layout_marginStart="@dimen/global_actions_side_margin"
   >
     <LinearLayout
         android:id="@android:id/list"
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
index aa2fe3c..57b3761 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java
@@ -16,7 +16,6 @@
 
 package com.android.keyguard;
 
-import android.app.Activity;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -31,6 +30,8 @@
 import android.view.KeyEvent;
 import android.widget.FrameLayout;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
@@ -101,7 +102,8 @@
     public static final boolean DEBUG = KeyguardConstants.DEBUG;
     private static final String TAG = "KeyguardViewBase";
 
-    private KeyguardSecurityContainer mSecurityContainer;
+    @VisibleForTesting
+    protected KeyguardSecurityContainer mSecurityContainer;
 
     public KeyguardHostView(Context context) {
         this(context, null);
@@ -446,4 +448,11 @@
     public SecurityMode getCurrentSecurityMode() {
         return mSecurityContainer.getCurrentSecurityMode();
     }
+
+    /**
+     * When bouncer was visible and is starting to become hidden.
+     */
+    public void onStartingToHide() {
+        mSecurityContainer.onStartingToHide();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 718bcf1..65bf7e6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -152,6 +152,11 @@
         mImm.hideSoftInputFromWindow(getWindowToken(), 0);
     }
 
+    @Override
+    public void onStartingToHide() {
+        mImm.hideSoftInputFromWindow(getWindowToken(), 0);
+    }
+
     private void updateSwitchImeButton() {
         // If there's more than one IME, enable the IME switcher button
         final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 502c078..9cfcc52 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -227,6 +227,13 @@
     }
 
     @Override
+    public void onStartingToHide() {
+        if (mCurrentSecuritySelection != SecurityMode.None) {
+            getSecurityView(mCurrentSecuritySelection).onStartingToHide();
+        }
+    }
+
+    @Override
     public boolean shouldDelayChildPressedState() {
         return true;
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
index 20b1e0d..43cef3a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -159,4 +159,9 @@
     default boolean disallowInterceptTouch(MotionEvent event) {
         return false;
     }
+
+    /**
+     * When bouncer was visible but is being dragged down or dismissed.
+     */
+    default void onStartingToHide() {};
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 367058f..a96ef91 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1301,6 +1301,7 @@
     private FingerprintManager mFpm;
     private FaceManager mFaceManager;
     private boolean mFingerprintLockedOut;
+    private TelephonyManager mTelephonyManager;
 
     /**
      * When we receive a
@@ -1728,10 +1729,22 @@
         }
         updateAirplaneModeState();
 
-        TelephonyManager telephony =
+        mTelephonyManager =
                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
-        if (telephony != null) {
-            telephony.listen(mPhoneStateListener, LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+        if (mTelephonyManager != null) {
+            mTelephonyManager.listen(mPhoneStateListener,
+                    LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE);
+            // Set initial sim states values.
+            for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) {
+                int state = mTelephonyManager.getSimState(slot);
+                int[] subIds = mSubscriptionManager.getSubscriptionIds(slot);
+                if (subIds != null) {
+                    for (int subId : subIds) {
+                        mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state)
+                                .sendToTarget();
+                    }
+                }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
index a7c4043..8f3dc22 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
@@ -19,8 +19,10 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
 import android.app.AlarmManager;
 import android.app.IActivityManager;
+import android.app.IActivityTaskManager;
 import android.app.IWallpaperManager;
 import android.app.KeyguardManager;
 import android.app.NotificationManager;
@@ -128,6 +130,12 @@
         return ActivityManager.getService();
     }
 
+    @Singleton
+    @Provides
+    static IActivityTaskManager provideIActivityTaskManager() {
+        return ActivityTaskManager.getService();
+    }
+
     @Provides
     @Singleton
     static IBatteryStats provideIBatteryStats() {
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 2c1bd21..3226605 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -78,6 +78,7 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
 import android.widget.ListPopupWindow;
 import android.widget.ListView;
 import android.widget.TextView;
@@ -2053,8 +2054,17 @@
             if (overflowButton != null) {
                 if (mOverflowAdapter.getCount() > 0) {
                     overflowButton.setOnClickListener((view) -> showPowerOverflowMenu());
+                    LinearLayout.LayoutParams params =
+                            (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams();
+                    params.setMarginEnd(0);
+                    mGlobalActionsLayout.setLayoutParams(params);
                 } else {
                     overflowButton.setVisibility(View.GONE);
+                    LinearLayout.LayoutParams params =
+                            (LinearLayout.LayoutParams) mGlobalActionsLayout.getLayoutParams();
+                    params.setMarginEnd(mContext.getResources().getDimensionPixelSize(
+                            com.android.systemui.R.dimen.global_actions_side_margin));
+                    mGlobalActionsLayout.setLayoutParams(params);
                 }
             }
 
@@ -2220,6 +2230,7 @@
                     })
                     .start();
             dismissPanel();
+            dismissOverflow();
             resetOrientation();
         }
 
@@ -2227,6 +2238,7 @@
             mShowing = false;
             if (mControlsUiController != null) mControlsUiController.hide();
             dismissPanel();
+            dismissOverflow();
             resetOrientation();
             completeDismiss();
         }
@@ -2243,6 +2255,12 @@
             }
         }
 
+        private void dismissOverflow() {
+            if (mOverflowPopup != null) {
+                mOverflowPopup.dismiss();
+            }
+        }
+
         private void setRotationSuggestionsEnabled(boolean enabled) {
             try {
                 final int userId = Binder.getCallingUserHandle().getIdentifier();
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 9217eb1..f25de6a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -21,20 +21,21 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.Icon;
 import android.graphics.drawable.RippleDrawable;
+import android.media.MediaDescription;
 import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
+import android.service.media.MediaBrowserService;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
@@ -55,6 +56,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.QSMediaBrowser;
 import com.android.systemui.util.Assert;
 
 import java.util.List;
@@ -67,7 +69,7 @@
     private static final String TAG = "MediaControlPanel";
     @Nullable private final LocalMediaManager mLocalMediaManager;
     private final Executor mForegroundExecutor;
-    private final Executor mBackgroundExecutor;
+    protected final Executor mBackgroundExecutor;
 
     private Context mContext;
     protected LinearLayout mMediaNotifView;
@@ -76,13 +78,18 @@
     private MediaController mController;
     private int mForegroundColor;
     private int mBackgroundColor;
-    protected ComponentName mRecvComponent;
     private MediaDevice mDevice;
+    protected ComponentName mServiceComponent;
     private boolean mIsRegistered = false;
     private String mKey;
 
     private final int[] mActionIds;
 
+    public static final String MEDIA_PREFERENCES = "media_control_prefs";
+    public static final String MEDIA_PREFERENCE_KEY = "browser_components";
+    private SharedPreferences mSharedPrefs;
+    private boolean mCheckedForResumption = false;
+
     // Button IDs used in notifications
     protected static final int[] NOTIF_ACTION_IDS = {
             com.android.internal.R.id.action0,
@@ -154,7 +161,6 @@
      * Initialize a new control panel
      * @param context
      * @param parent
-     * @param manager
      * @param routeManager Manager used to listen for device change events.
      * @param layoutId layout resource to use for this control panel
      * @param actionIds resource IDs for action buttons in the layout
@@ -198,47 +204,50 @@
     /**
      * Update the media panel view for the given media session
      * @param token
-     * @param icon
+     * @param iconDrawable
      * @param iconColor
      * @param bgColor
      * @param contentIntent
      * @param appNameString
      * @param key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
+    public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, int iconColor,
             int bgColor, PendingIntent contentIntent, String appNameString, String key) {
-        mToken = token;
+        // Ensure that component names are updated if token has changed
+        if (mToken == null || !mToken.equals(token)) {
+            mToken = token;
+            mServiceComponent = null;
+            mCheckedForResumption = false;
+        }
+
         mForegroundColor = iconColor;
         mBackgroundColor = bgColor;
         mController = new MediaController(mContext, mToken);
         mKey = key;
 
-        MediaMetadata mediaMetadata = mController.getMetadata();
-
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
+        // Try to find a browser service component for this app
+        // TODO also check for a media button receiver intended for restarting (b/154127084)
+        // Only check if we haven't tried yet or the session token changed
+        String pkgName = mController.getPackageName();
+        if (mServiceComponent == null && !mCheckedForResumption) {
+            Log.d(TAG, "Checking for service component");
+            PackageManager pm = mContext.getPackageManager();
+            Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
+            List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
+            if (resumeInfo != null) {
+                for (ResolveInfo inf : resumeInfo) {
+                    if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
+                        mBackgroundExecutor.execute(() ->
+                                tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
+                        break;
+                    }
                 }
             }
+            mCheckedForResumption = true;
         }
 
         mController.registerCallback(mSessionCallback);
 
-        if (mediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
-            return;
-        }
-
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumView != null) {
-            // Resize art in a background thread
-            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
-        }
         mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
 
         // Click action
@@ -256,32 +265,9 @@
 
         // App icon
         ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
         iconDrawable.setTint(mForegroundColor);
         appIcon.setImageDrawable(iconDrawable);
 
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(mForegroundColor);
-
-        // Not in mini player:
-        // App title
-        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
-        if (appName != null) {
-            appName.setText(appNameString);
-            appName.setTextColor(mForegroundColor);
-        }
-
-        // Artist name
-        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
-        if (artistText != null) {
-            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
-            artistText.setText(artistName);
-            artistText.setTextColor(mForegroundColor);
-        }
-
         // Transfer chip
         mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
         if (mSeamless != null && mLocalMediaManager != null) {
@@ -300,6 +286,39 @@
         }
 
         makeActive();
+
+        // App title (not in mini player)
+        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
+        if (appName != null) {
+            appName.setText(appNameString);
+            appName.setTextColor(mForegroundColor);
+        }
+
+        MediaMetadata mediaMetadata = mController.getMetadata();
+        if (mediaMetadata == null) {
+            Log.e(TAG, "Media metadata was null");
+            return;
+        }
+
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
+        }
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+        titleText.setText(songName);
+        titleText.setTextColor(mForegroundColor);
+
+        // Artist name (not in mini player)
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            artistText.setText(artistName);
+            artistText.setTextColor(mForegroundColor);
+        }
     }
 
     /**
@@ -320,9 +339,12 @@
 
     /**
      * Get the name of the package associated with the current media controller
-     * @return the package name
+     * @return the package name, or null if no controller
      */
     public String getMediaPlayerPackage() {
+        if (mController == null) {
+            return null;
+        }
         return mController.getPackageName();
     }
 
@@ -370,11 +392,27 @@
 
     /**
      * Process album art for layout
+     * @param description media description
+     * @param albumView view to hold the album art
+     */
+    protected void processAlbumArt(MediaDescription description, ImageView albumView) {
+        Bitmap albumArt = description.getIconBitmap();
+        //TODO check other fields (b/151054111, b/152067055)
+        processAlbumArtInternal(albumArt, albumView);
+    }
+
+    /**
+     * Process album art for layout
      * @param metadata media metadata
      * @param albumView view to hold the album art
      */
     private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
         Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        //TODO check other fields (b/151054111, b/152067055)
+        processAlbumArtInternal(albumArt, albumView);
+    }
+
+    private void processAlbumArtInternal(Bitmap albumArt, ImageView albumView) {
         float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
         RoundedBitmapDrawable roundedDrawable = null;
         if (albumArt != null) {
@@ -449,10 +487,24 @@
     }
 
     /**
-     * Put controls into a resumption state
+     * Puts controls into a resumption state if possible, or calls removePlayer if no component was
+     * found that could resume playback
      */
     public void clearControls() {
         Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
+        if (mServiceComponent == null) {
+            // If we don't have a way to resume, just remove the player altogether
+            Log.d(TAG, "Removing unresumable controls");
+            removePlayer();
+            return;
+        }
+        resetButtons();
+    }
+
+    /**
+     * Hide the media buttons and show only a restart button
+     */
+    protected void resetButtons() {
         // Hide all the old buttons
         for (int i = 0; i < mActionIds.length; i++) {
             ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
@@ -465,27 +517,8 @@
         ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
         btn.setOnClickListener(v -> {
             Log.d(TAG, "Attempting to restart session");
-            // Send a media button event to previously found receiver
-            if (mRecvComponent != null) {
-                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                intent.setComponent(mRecvComponent);
-                int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                intent.putExtra(
-                        Intent.EXTRA_KEY_EVENT,
-                        new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                mContext.sendBroadcast(intent);
-            } else {
-                // If we don't have a receiver, try relaunching the activity instead
-                if (mController.getSessionActivity() != null) {
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled", e);
-                    }
-                } else {
-                    Log.e(TAG, "No receiver or activity to restart");
-                }
-            }
+            QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent);
+            browser.restart();
         });
         btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
         btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
@@ -514,4 +547,65 @@
         }
     }
 
+    /**
+     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
+     * component to the list of resumption components
+     */
+    private void tryUpdateResumptionList(ComponentName componentName) {
+        Log.d(TAG, "Testing if we can connect to " + componentName);
+        QSMediaBrowser.testConnection(mContext,
+                new QSMediaBrowser.Callback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "yes we can resume with " + componentName);
+                        mServiceComponent = componentName;
+                        updateResumptionList(componentName);
+                    }
+
+                    @Override
+                    public void onError() {
+                        Log.d(TAG, "Cannot resume with " + componentName);
+                        mServiceComponent = null;
+                        clearControls();
+                        // remove
+                    }
+                },
+                componentName);
+    }
+
+    /**
+     * Add the component to the saved list of media browser services, checking for duplicates and
+     * removing older components that exceed the maximum limit
+     * @param componentName
+     */
+    private synchronized void updateResumptionList(ComponentName componentName) {
+        // Add to front of saved list
+        if (mSharedPrefs == null) {
+            mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
+        }
+        String componentString = componentName.flattenToString();
+        String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
+        if (listString == null) {
+            listString = componentString;
+        } else {
+            String[] components = listString.split(QSMediaBrowser.DELIMITER);
+            StringBuilder updated = new StringBuilder(componentString);
+            int nBrowsers = 1;
+            for (int i = 0; i < components.length
+                    && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
+                if (componentString.equals(components[i])) {
+                    continue;
+                }
+                updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
+                nBrowsers++;
+            }
+            listString = updated.toString();
+        }
+        mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
+    }
+
+    /**
+     * Called when a player can't be resumed to give it an opportunity to hide or remove itself
+     */
+    protected void removePlayer() { }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
new file mode 100644
index 0000000..302b8420
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
@@ -0,0 +1,259 @@
+/*
+ * 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.systemui.qs;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.MediaDescription;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Media browser for managing resumption in QS media controls
+ */
+public class QSMediaBrowser {
+
+    /** Maximum number of controls to show on boot */
+    public static final int MAX_RESUMPTION_CONTROLS = 5;
+
+    /** Delimiter for saved component names */
+    public static final String DELIMITER = ":";
+
+    private static final String TAG = "QSMediaBrowser";
+    private final Context mContext;
+    private final Callback mCallback;
+    private MediaBrowser mMediaBrowser;
+    private ComponentName mComponentName;
+
+    /**
+     * Initialize a new media browser
+     * @param context the context
+     * @param callback used to report media items found
+     * @param componentName Component name of the MediaBrowserService this browser will connect to
+     */
+    public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+        mContext = context;
+        mCallback = callback;
+        mComponentName = componentName;
+
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = new MediaBrowser(mContext,
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+    }
+
+    /**
+     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned
+     * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription
+     */
+    public void findRecentMedia() {
+        Log.d(TAG, "Connecting to " + mComponentName);
+        mMediaBrowser.connect();
+    }
+
+    private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
+            new MediaBrowser.SubscriptionCallback() {
+        @Override
+        public void onChildrenLoaded(String parentId,
+                List<MediaBrowser.MediaItem> children) {
+            if (children.size() == 0) {
+                Log.e(TAG, "No children found");
+                return;
+            }
+            // We ask apps to return a playable item as the first child when sending
+            // a request with EXTRA_RECENT; if they don't, no resume controls
+            MediaBrowser.MediaItem child = children.get(0);
+            MediaDescription desc = child.getDescription();
+            if (child.isPlayable()) {
+                mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
+            } else {
+                Log.e(TAG, "Child found but not playable for " + mComponentName);
+            }
+            mMediaBrowser.disconnect();
+        }
+
+        @Override
+        public void onError(String parentId) {
+            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
+            mMediaBrowser.disconnect();
+        }
+
+        @Override
+        public void onError(String parentId, Bundle options) {
+            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+                    + ", options: " + options);
+            mMediaBrowser.disconnect();
+        }
+    };
+
+    private final MediaBrowser.ConnectionCallback mConnectionCallback =
+            new MediaBrowser.ConnectionCallback() {
+        /**
+         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+         * For resumption controls, apps are expected to return a playable media item as the first
+         * child. If there are no children or it isn't playable it will be ignored.
+         */
+        @Override
+        public void onConnected() {
+            if (mMediaBrowser.isConnected()) {
+                mCallback.onConnected();
+                Log.d(TAG, "Service connected for " + mComponentName);
+                String root = mMediaBrowser.getRoot();
+                mMediaBrowser.subscribe(root, mSubscriptionCallback);
+            }
+        }
+
+        /**
+         * Invoked when the client is disconnected from the media browser.
+         */
+        @Override
+        public void onConnectionSuspended() {
+            Log.d(TAG, "Connection suspended for " + mComponentName);
+        }
+
+        /**
+         * Invoked when the connection to the media browser failed.
+         */
+        @Override
+        public void onConnectionFailed() {
+            Log.e(TAG, "Connection failed for " + mComponentName);
+            mCallback.onError();
+        }
+    };
+
+    /**
+     * Connects to the MediaBrowserService and starts playback
+     */
+    public void restart() {
+        if (mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        mMediaBrowser = new MediaBrowser(mContext, mComponentName,
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected());
+                        MediaSession.Token token = mMediaBrowser.getSessionToken();
+                        MediaController controller = new MediaController(mContext, token);
+                        controller.getTransportControls();
+                        controller.getTransportControls().prepare();
+                        controller.getTransportControls().play();
+                    }
+                }, rootHints);
+        mMediaBrowser.connect();
+    }
+
+    /**
+     * Get the media session token
+     * @return the token, or null if the MediaBrowser is null or disconnected
+     */
+    public MediaSession.Token getToken() {
+        if (mMediaBrowser == null || !mMediaBrowser.isConnected()) {
+            return null;
+        }
+        return mMediaBrowser.getSessionToken();
+    }
+
+    /**
+     * Get an intent to launch the app associated with this browser service
+     * @return
+     */
+    public PendingIntent getAppIntent() {
+        PackageManager pm = mContext.getPackageManager();
+        Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
+        return PendingIntent.getActivity(mContext, 0, launchIntent, 0);
+    }
+
+    /**
+     * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser
+     * @param mContext the context
+     * @param callback methods onConnected or onError will be called to indicate whether the
+     *                 connection was successful or not
+     * @param mComponentName Component name of the MediaBrowserService this browser will connect to
+     */
+    public static MediaBrowser testConnection(Context mContext, Callback callback,
+            ComponentName mComponentName) {
+        final MediaBrowser.ConnectionCallback mConnectionCallback =
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        Log.d(TAG, "connected");
+                        callback.onConnected();
+                    }
+
+                    @Override
+                    public void onConnectionSuspended() {
+                        Log.d(TAG, "suspended");
+                        callback.onError();
+                    }
+
+                    @Override
+                    public void onConnectionFailed() {
+                        Log.d(TAG, "failed");
+                        callback.onError();
+                    }
+                };
+        Bundle rootHints = new Bundle();
+        rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
+        MediaBrowser browser = new MediaBrowser(mContext,
+                mComponentName,
+                mConnectionCallback,
+                rootHints);
+        browser.connect();
+        return browser;
+    }
+
+    /**
+     * Interface to handle results from QSMediaBrowser
+     */
+    public static class Callback {
+        /**
+         * Called when the browser has successfully connected to the service
+         */
+        public void onConnected() {
+        }
+
+        /**
+         * Called when the browser encountered an error connecting to the service
+         */
+        public void onError() {
+        }
+
+        /**
+         * Called when the browser finds a suitable track to add to the media carousel
+         * @param track media info for the item
+         * @param component component of the MediaBrowserService which returned this
+         * @param browser reference to the browser
+         */
+        public void addTrack(MediaDescription track, ComponentName component,
+                QSMediaBrowser browser) {
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
index 89b22bc..0f06566 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
@@ -18,11 +18,12 @@
 
 import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle;
 
-import android.app.Notification;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
+import android.media.MediaDescription;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.util.Log;
@@ -60,9 +61,11 @@
     };
 
     private final QSPanel mParent;
+    private final Executor mForegroundExecutor;
     private final DelayableExecutor mBackgroundExecutor;
     private final SeekBarViewModel mSeekBarViewModel;
     private final SeekBarObserver mSeekBarObserver;
+    private String mPackageName;
 
     /**
      * Initialize quick shade version of player
@@ -77,6 +80,7 @@
         super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS,
                 foregroundExecutor, backgroundExecutor);
         mParent = (QSPanel) parent;
+        mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
         mSeekBarObserver = new SeekBarObserver(getView());
@@ -90,47 +94,101 @@
     }
 
     /**
+     * Add a media panel view based on a media description. Used for resumption
+     * @param description
+     * @param iconColor
+     * @param bgColor
+     * @param contentIntent
+     * @param pkgName
+     */
+    public void setMediaSession(MediaSession.Token token, MediaDescription description,
+            int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) {
+        mPackageName = pkgName;
+        PackageManager pm = getContext().getPackageManager();
+        Drawable icon = null;
+        CharSequence appName = pkgName.substring(pkgName.lastIndexOf("."));
+        try {
+            icon = pm.getApplicationIcon(pkgName);
+            appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Error getting package information", e);
+        }
+
+        // Set what we can normally
+        super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName.toString(),
+                null);
+
+        // Then add info from MediaDescription
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView));
+        }
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        CharSequence songName = description.getTitle();
+        titleText.setText(songName);
+        titleText.setTextColor(iconColor);
+
+        // Artist name (not in mini player)
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            CharSequence artistName = description.getSubtitle();
+            artistText.setText(artistName);
+            artistText.setTextColor(iconColor);
+        }
+
+        initLongPressMenu(iconColor);
+
+        // Set buttons to resume state
+        resetButtons();
+    }
+
+    /**
      * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
      * @param iconColor foreground color (for text, icons)
      * @param bgColor background color
      * @param actionsContainer a LinearLayout containing the media action buttons
-     * @param notif reference to original notification
+     * @param contentIntent Intent to send when user taps on player
+     * @param appName Application title
      * @param key original notification's key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
-            int bgColor, View actionsContainer, Notification notif, String key) {
+    public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor,
+            int bgColor, View actionsContainer, PendingIntent contentIntent, String appName,
+            String key) {
 
-        String appName = Notification.Builder.recoverBuilder(getContext(), notif)
-                .loadHeaderAppName();
-        super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, appName, key);
+        super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, appName, key);
 
         // Media controls
-        LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        int i = 0;
-        for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
-            ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
-            if (thatBtn == null || thatBtn.getDrawable() == null
-                    || thatBtn.getVisibility() != View.VISIBLE) {
-                thisBtn.setVisibility(View.GONE);
-                continue;
+        if (actionsContainer != null) {
+            LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
+            int i = 0;
+            for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
+                ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
+                ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
+                if (thatBtn == null || thatBtn.getDrawable() == null
+                        || thatBtn.getVisibility() != View.VISIBLE) {
+                    thisBtn.setVisibility(View.GONE);
+                    continue;
+                }
+
+                Drawable thatIcon = thatBtn.getDrawable();
+                thisBtn.setImageDrawable(thatIcon.mutate());
+                thisBtn.setVisibility(View.VISIBLE);
+                thisBtn.setOnClickListener(v -> {
+                    Log.d(TAG, "clicking on other button");
+                    thatBtn.performClick();
+                });
             }
 
-            Drawable thatIcon = thatBtn.getDrawable();
-            thisBtn.setImageDrawable(thatIcon.mutate());
-            thisBtn.setVisibility(View.VISIBLE);
-            thisBtn.setOnClickListener(v -> {
-                Log.d(TAG, "clicking on other button");
-                thatBtn.performClick();
-            });
-        }
-
-        // Hide any unused buttons
-        for (; i < QS_ACTION_IDS.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
-            thisBtn.setVisibility(View.GONE);
+            // Hide any unused buttons
+            for (; i < QS_ACTION_IDS.length; i++) {
+                ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
+                thisBtn.setVisibility(View.GONE);
+            }
         }
 
         // Seek Bar
@@ -138,6 +196,10 @@
         mBackgroundExecutor.execute(
                 () -> mSeekBarViewModel.updateController(controller, iconColor));
 
+        initLongPressMenu(iconColor);
+    }
+
+    private void initLongPressMenu(int iconColor) {
         // Set up long press menu
         View guts = mMediaNotifView.findViewById(R.id.media_guts);
         View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
@@ -145,7 +207,7 @@
 
         View clearView = options.findViewById(R.id.remove);
         clearView.setOnClickListener(b -> {
-            mParent.removeMediaPlayer(QSMediaPlayer.this);
+            removePlayer();
         });
         ImageView removeIcon = options.findViewById(R.id.remove_icon);
         removeIcon.setImageTintList(ColorStateList.valueOf(iconColor));
@@ -165,11 +227,9 @@
     }
 
     @Override
-    public void clearControls() {
-        super.clearControls();
-
+    protected void resetButtons() {
+        super.resetButtons();
         mSeekBarViewModel.clearController();
-
         View guts = mMediaNotifView.findViewById(R.id.media_guts);
         View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options);
 
@@ -192,4 +252,19 @@
     public void setListening(boolean listening) {
         mSeekBarViewModel.setListening(listening);
     }
+
+    @Override
+    public void removePlayer() {
+        Log.d(TAG, "removing player from parent: " + mParent);
+        // Ensure this happens on the main thread (could happen in QSMediaBrowser callback)
+        mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this));
+    }
+
+    @Override
+    public String getMediaPlayerPackage() {
+        if (getController() == null) {
+            return mPackageName;
+        }
+        return super.getMediaPlayerPackage();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index fee0838..1eb5778 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -21,16 +21,25 @@
 import static com.android.systemui.util.Utils.useQsMediaPlayer;
 
 import android.annotation.Nullable;
+import android.app.Notification;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.drawable.Icon;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.media.MediaDescription;
 import android.media.session.MediaSession;
 import android.metrics.LogMaker;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.notification.StatusBarNotification;
 import android.service.quicksettings.Tile;
 import android.util.AttributeSet;
@@ -54,6 +63,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.media.MediaControlPanel;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
@@ -90,6 +100,7 @@
 
     protected final Context mContext;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
+    private final BroadcastDispatcher mBroadcastDispatcher;
     private String mCachedSpecs = "";
     protected final View mBrightnessView;
     private final H mHandler = new H();
@@ -123,6 +134,19 @@
 
     private BrightnessMirrorController mBrightnessMirrorController;
     private View mDivider;
+    private boolean mHasLoadedMediaControls;
+
+    private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
+                if (!mHasLoadedMediaControls) {
+                    loadMediaResumptionControls();
+                }
+            }
+        }
+    };
 
     @Inject
     public QSPanel(
@@ -142,6 +166,7 @@
         mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mLocalBluetoothManager = localBluetoothManager;
+        mBroadcastDispatcher = broadcastDispatcher;
 
         setOrientation(VERTICAL);
 
@@ -176,7 +201,7 @@
         updateResources();
 
         mBrightnessController = new BrightnessController(getContext(),
-                findViewById(R.id.brightness_slider), broadcastDispatcher);
+                findViewById(R.id.brightness_slider), mBroadcastDispatcher);
     }
 
     @Override
@@ -206,7 +231,7 @@
      * @param notif
      * @param key
      */
-    public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
+    public void addMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor,
             View actionsContainer, StatusBarNotification notif, String key) {
         if (!useQsMediaPlayer(mContext)) {
             // Shouldn't happen, but just in case
@@ -221,7 +246,14 @@
         QSMediaPlayer player = null;
         String packageName = notif.getPackageName();
         for (QSMediaPlayer p : mMediaPlayers) {
-            if (p.getMediaSessionToken().equals(token)) {
+            if (p.getKey() == null) {
+                // No notification key = loaded via mediabrowser, so just match on package
+                if (packageName.equals(p.getMediaPlayerPackage())) {
+                    Log.d(TAG, "Found matching resume player by package: " + packageName);
+                    player = p;
+                    break;
+                }
+            } else if (p.getMediaSessionToken().equals(token)) {
                 Log.d(TAG, "Found matching player by token " + packageName);
                 player = p;
                 break;
@@ -262,8 +294,10 @@
         }
 
         Log.d(TAG, "setting player session");
+        String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification())
+                .loadHeaderAppName();
         player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
-                notif.getNotification(), key);
+                notif.getNotification().contentIntent, appName, key);
 
         if (mMediaPlayers.size() > 0) {
             ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
@@ -293,6 +327,74 @@
         return true;
     }
 
+    private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() {
+        @Override
+        public void addTrack(MediaDescription desc, ComponentName component,
+                QSMediaBrowser browser) {
+            if (component == null) {
+                Log.e(TAG, "Component cannot be null");
+                return;
+            }
+
+            Log.d(TAG, "adding track from browser: " + desc + ", " + component);
+            QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this,
+                    null, mForegroundExecutor, mBackgroundExecutor);
+
+            String pkgName = component.getPackageName();
+
+            // Add controls to carousel
+            int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width);
+            int padding = (int) getResources().getDimension(R.dimen.qs_media_padding);
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth,
+                    LayoutParams.MATCH_PARENT);
+            lp.setMarginStart(padding);
+            lp.setMarginEnd(padding);
+            mMediaCarousel.addView(player.getView(), lp);
+            ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE);
+            mMediaPlayers.add(player);
+
+            int iconColor = Color.DKGRAY;
+            int bgColor = Color.LTGRAY;
+
+            MediaSession.Token token = browser.getToken();
+            player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(),
+                    pkgName);
+        }
+    };
+
+    /**
+     * Load controls for resuming media, if available
+     */
+    private void loadMediaResumptionControls() {
+        if (!useQsMediaPlayer(mContext)) {
+            return;
+        }
+        Log.d(TAG, "Loading resumption controls");
+
+        //  Look up saved components to resume
+        Context userContext = mContext.createContextAsUser(mContext.getUser(), 0);
+        SharedPreferences prefs = userContext.getSharedPreferences(
+                MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE);
+        String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null);
+        if (listString == null) {
+            Log.d(TAG, "No saved media components");
+            return;
+        }
+
+        String[] components = listString.split(QSMediaBrowser.DELIMITER);
+        Log.d(TAG, "components are: " + listString + " count " + components.length);
+        for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
+            String[] info = components[i].split("/");
+            String packageName = info[0];
+            String className = info[1];
+            ComponentName component = new ComponentName(packageName, className);
+            QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback,
+                    component);
+            browser.findRecentMedia();
+        }
+        mHasLoadedMediaControls = true;
+    }
+
     protected void addDivider() {
         mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
         mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
@@ -343,6 +445,22 @@
             mBrightnessMirrorController.addCallback(this);
         }
         mDumpManager.registerDumpable(getDumpableTag(), this);
+
+        if (getClass() == QSPanel.class) {
+            //TODO(ethibodeau) remove class check after media refactor in ag/11059751
+            // Only run this in QSPanel proper, not QQS
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_USER_UNLOCKED);
+            mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null,
+                    UserHandle.ALL);
+            mHasLoadedMediaControls = false;
+
+            UserManager userManager = mContext.getSystemService(UserManager.class);
+            if (userManager.isUserUnlocked(mContext.getUserId())) {
+                // If it's already unlocked (like if dark theme was toggled), we can load now
+                loadMediaResumptionControls();
+            }
+        }
     }
 
     @Override
@@ -358,6 +476,7 @@
             mBrightnessMirrorController.removeCallback(this);
         }
         mDumpManager.unregisterDumpable(getDumpableTag());
+        mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver);
         super.onDetachedFromWindow();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
index 6229672..7ba7c5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
@@ -19,7 +19,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.view.View;
@@ -67,7 +66,7 @@
      * @param contentIntent Intent to send when user taps on the view
      * @param key original notification's key
      */
-    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
+    public void setMediaSession(MediaSession.Token token, Drawable icon, int iconColor, int bgColor,
             View actionsContainer, int[] actionsToShow, PendingIntent contentIntent, String key) {
         // Only update if this is a different session and currently playing
         String oldPackage = "";
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
new file mode 100644
index 0000000..5ced40c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -0,0 +1,61 @@
+/*
+ * 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.systemui.screenshot;
+
+import android.os.IBinder;
+import android.view.IWindowManager;
+
+import javax.inject.Inject;
+
+/**
+ * Stub
+ */
+public class ScrollCaptureController {
+
+    public static final int STATUS_A = 0;
+    public static final int STATUS_B = 1;
+
+    private final IWindowManager mWindowManagerService;
+    private StatusListener mListener;
+
+    /**
+     *
+     * @param windowManagerService
+     */
+    @Inject
+    public ScrollCaptureController(IWindowManager windowManagerService) {
+        mWindowManagerService = windowManagerService;
+    }
+
+    interface StatusListener {
+        void onScrollCaptureStatus(boolean available);
+    }
+
+    /**
+     *
+     * @param window
+     * @param listener
+     */
+    public void getStatus(IBinder window, StatusListener listener) {
+        mListener = listener;
+//        try {
+//           mWindowManagerService.requestScrollCapture(window, new ClientCallbacks());
+//        } catch (RemoteException e) {
+//        }
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
index fa1b026..4de978c 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt
@@ -18,8 +18,10 @@
 
 import android.content.Context
 import android.os.UserHandle
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.util.Assert
+import java.lang.IllegalStateException
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -32,7 +34,16 @@
     broadcastDispatcher: BroadcastDispatcher
 ) {
     private val userTracker: CurrentUserTracker
-    var currentUserContext: Context
+    private var initialized = false
+
+    private var _curUserContext: Context? = null
+    val currentUserContext: Context
+        get() {
+            if (!initialized) {
+                throw IllegalStateException("Must initialize before getting context")
+            }
+            return _curUserContext!!
+        }
 
     init {
         userTracker = object : CurrentUserTracker(broadcastDispatcher) {
@@ -40,21 +51,21 @@
                 handleUserSwitched(newUserId)
             }
         }
-
-        currentUserContext = makeUserContext(userTracker.currentUserId)
     }
 
     fun initialize() {
+        initialized = true
+        _curUserContext = makeUserContext(userTracker.currentUserId)
         userTracker.startTracking()
     }
 
-    private fun handleUserSwitched(newUserId: Int) {
-        currentUserContext = makeUserContext(newUserId)
+    @VisibleForTesting
+    fun handleUserSwitched(newUserId: Int) {
+        _curUserContext = makeUserContext(newUserId)
     }
 
     private fun makeUserContext(uid: Int): Context {
         Assert.isMainThread()
-        return sysuiContext.createContextAsUser(
-                UserHandle.getUserHandleForUid(userTracker.currentUserId), 0)
+        return sysuiContext.createContextAsUser(UserHandle.of(uid), 0)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
index 85dcbb6..5aa7946 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java
@@ -180,12 +180,17 @@
             if (isHomeOrRecentTask(rootTask)) {
                 tiles.mHomeAndRecentsSurfaces.add(rootTask.token.getLeash());
             }
+            // Only move resizeable task to split secondary. WM will just ignore this anyways...
+            if (!rootTask.isResizable()) continue;
+            // Only move fullscreen tasks to split secondary.
             if (rootTask.configuration.windowConfiguration.getWindowingMode()
                     != WINDOWING_MODE_FULLSCREEN) {
                 continue;
             }
             wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */);
         }
+        // Move the secondary split-forward.
+        wct.reorder(tiles.mSecondary.token, true /* onTop */);
         boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct);
         WindowOrganizer.applyTransaction(wct);
         return isHomeResizable;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 37fc13e..afb5002 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -123,7 +123,6 @@
                 res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
         mBubbleController = bubbleController;
         mDynamicPrivacyController = privacyController;
-        privacyController.addListener(this);
         mDynamicChildBindController = dynamicChildBindController;
     }
 
@@ -131,6 +130,7 @@
             NotificationListContainer listContainer) {
         mPresenter = presenter;
         mListContainer = listContainer;
+        mDynamicPrivacyController.addListener(this);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 5236385..cb0c283 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -378,6 +378,7 @@
     /**
      * Returns the data needed for a bubble for this notification, if it exists.
      */
+    @Nullable
     public Notification.BubbleMetadata getBubbleMetadata() {
         return mBubbleMetadata;
     }
@@ -385,7 +386,7 @@
     /**
      * Sets bubble metadata for this notification.
      */
-    public void setBubbleMetadata(Notification.BubbleMetadata metadata) {
+    public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) {
         mBubbleMetadata = metadata;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
index 6d14ccf..9b6ae9a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java
@@ -175,7 +175,7 @@
     private OnHeadsUpChangedListener mOnHeadsUpChangedListener  = new OnHeadsUpChangedListener() {
         @Override
         public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {
-            if (!isHeadsUp) {
+            if (!isHeadsUp && !entry.getRow().isRemoved()) {
                 mHeadsUpViewBinder.unbindHeadsUpView(entry);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index 55a5935..bcc81a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -31,6 +31,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.INotificationManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -98,7 +99,7 @@
     private ShortcutInfo mShortcutInfo;
     private String mConversationId;
     private StatusBarNotification mSbn;
-    private Notification.BubbleMetadata mBubbleMetadata;
+    @Nullable private Notification.BubbleMetadata mBubbleMetadata;
     private Context mUserContext;
     private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider;
     private boolean mIsDeviceProvisioned;
@@ -203,6 +204,7 @@
             String pkg,
             NotificationChannel notificationChannel,
             NotificationEntry entry,
+            Notification.BubbleMetadata bubbleMetadata,
             OnSettingsClickListener onSettingsClick,
             OnSnoozeClickListener onSnoozeClickListener,
             ConversationIconFactory conversationIconFactory,
@@ -224,7 +226,7 @@
         mOnSnoozeClickListener = onSnoozeClickListener;
         mIconFactory = conversationIconFactory;
         mUserContext = userContext;
-        mBubbleMetadata = entry.getBubbleMetadata();
+        mBubbleMetadata = bubbleMetadata;
         mBuilderProvider = builderProvider;
 
         mShortcutManager = shortcutManager;
@@ -538,7 +540,8 @@
             Log.e(TAG, "Could not check conversation senders", e);
         }
 
-        boolean showAsBubble = mBubbleMetadata.getAutoExpandBubble()
+        boolean showAsBubble = mBubbleMetadata != null
+                && mBubbleMetadata.getAutoExpandBubble()
                 && Settings.Global.getInt(mContext.getContentResolver(),
                         NOTIFICATION_BUBBLES, 0) == 1;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 624fabc..1c808cf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -366,7 +366,8 @@
             final ExpandableNotificationRow row,
             NotificationConversationInfo notificationInfoView) throws Exception {
         NotificationGuts guts = row.getGuts();
-        StatusBarNotification sbn = row.getEntry().getSbn();
+        NotificationEntry entry = row.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
         String packageName = sbn.getPackageName();
         // Settings link is only valid for notifications that specify a non-system user
         NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null;
@@ -407,8 +408,9 @@
                 mNotificationManager,
                 mVisualStabilityManager,
                 packageName,
-                row.getEntry().getChannel(),
-                row.getEntry(),
+                entry.getChannel(),
+                entry,
+                entry.getBubbleMetadata(),
                 onSettingsClick,
                 onSnoozeClickListener,
                 iconFactoryLoader,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index 2da2724..796f22c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -22,6 +22,7 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
 import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
@@ -187,8 +188,9 @@
                     com.android.systemui.R.id.quick_qs_panel);
             StatusBarNotification sbn = mRow.getEntry().getSbn();
             Notification notif = sbn.getNotification();
+            Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext);
             panel.getMediaPlayer().setMediaSession(token,
-                    notif.getSmallIcon(),
+                    iconDrawable,
                     tintColor,
                     mBackgroundColor,
                     mActions,
@@ -198,7 +200,7 @@
             QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById(
                     com.android.systemui.R.id.quick_settings_panel);
             bigPanel.addMediaSession(token,
-                    notif.getSmallIcon(),
+                    iconDrawable,
                     tintColor,
                     mBackgroundColor,
                     mActions,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 90bc075b..ae7867d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -85,6 +85,8 @@
 import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory;
 import com.android.systemui.tuner.TunerService;
 
+import java.util.concurrent.Executor;
+
 /**
  * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status
  * text.
@@ -553,7 +555,7 @@
             }
         };
         if (!mKeyguardStateController.canDismissLockScreen()) {
-            AsyncTask.execute(runnable);
+            Dependency.get(Executor.class).execute(runnable);
         } else {
             boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr)
                     && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index 82e02b4..39949c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -400,6 +400,9 @@
             mExpansionCallback.onFullyHidden();
         } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) {
             mExpansionCallback.onStartingToHide();
+            if (mKeyguardView != null) {
+                mKeyguardView.onStartingToHide();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
index 051bd29..a284335 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java
@@ -357,7 +357,18 @@
         mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler);
         mListening = true;
 
+        // Initial setup of connectivity. Handled as if we had received a sticky broadcast of
+        // ConnectivityManager.CONNECTIVITY_ACTION or ConnectivityManager.INET_CONDITION_ACTION.
+        mReceiverHandler.post(this::updateConnectivity);
+
+        // Initial setup of WifiSignalController. Handled as if we had received a sticky broadcast
+        // of WifiManager.WIFI_STATE_CHANGED_ACTION or WifiManager.NETWORK_STATE_CHANGED_ACTION
+        mReceiverHandler.post(mWifiSignalController::fetchInitialState);
         updateMobileControllers();
+
+        // Initial setup of emergency information. Handled as if we had received a sticky broadcast
+        // of TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED.
+        mReceiverHandler.post(this::recalculateEmergency);
     }
 
     private void unregisterListeners() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
index b258fd4..5257ce4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
@@ -102,6 +102,20 @@
     }
 
     /**
+     * Fetches wifi initial state replacing the initial sticky broadcast.
+     */
+    public void fetchInitialState() {
+        mWifiTracker.fetchInitialState();
+        mCurrentState.enabled = mWifiTracker.enabled;
+        mCurrentState.connected = mWifiTracker.connected;
+        mCurrentState.ssid = mWifiTracker.ssid;
+        mCurrentState.rssi = mWifiTracker.rssi;
+        mCurrentState.level = mWifiTracker.level;
+        mCurrentState.statusLabel = mWifiTracker.statusLabel;
+        notifyListenersIfNecessary();
+    }
+
+    /**
      * Extract wifi state directly from broadcasts about changes in wifi state.
      */
     public void handleBroadcast(Intent intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
index e5da603..899aabb 100644
--- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
+++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java
@@ -32,6 +32,7 @@
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.IWindowManager;
 import android.view.IWindowSession;
@@ -352,5 +353,14 @@
 
         @Override
         public void dispatchPointerCaptureChanged(boolean hasCapture) {}
+
+        @Override
+        public void requestScrollCapture(IScrollCaptureController controller) {
+            try {
+                controller.onClientUnavailable();
+            } catch (RemoteException ex) {
+                // ignore
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
index 25f279b..dd5c833 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java
@@ -17,30 +17,50 @@
 package com.android.keyguard;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import com.android.internal.widget.LockPatternUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class KeyguardHostViewTest extends SysuiTestCase {
 
+    @Mock
+    private KeyguardSecurityContainer mSecurityContainer;
+    @Mock
+    private LockPatternUtils mLockPatternUtils;
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
     private KeyguardHostView mKeyguardHostView;
 
     @Before
     public void setup() {
         mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
-        mKeyguardHostView = new KeyguardHostView(getContext());
+        mKeyguardHostView = new KeyguardHostView(getContext()) {
+            @Override
+            protected void onFinishInflate() {
+                mSecurityContainer = KeyguardHostViewTest.this.mSecurityContainer;
+                mLockPatternUtils = KeyguardHostViewTest.this.mLockPatternUtils;
+            }
+        };
+        mKeyguardHostView.onFinishInflate();
     }
 
     @Test
@@ -50,4 +70,10 @@
                 null /* cancelAction */);
         Assert.assertTrue("Action should exist", mKeyguardHostView.hasDismissActions());
     }
+
+    @Test
+    public void testOnStartingToHide() {
+        mKeyguardHostView.onStartingToHide();
+        verify(mSecurityContainer).onStartingToHide();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 7403a11..6c00eca 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -61,6 +61,7 @@
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
@@ -133,20 +134,23 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
-    private Executor mBackgroundExecutor;
-    @Mock
     private RingerModeTracker mRingerModeTracker;
     @Mock
     private LiveData<Integer> mRingerModeLiveData;
+    @Mock
+    private TelephonyManager mTelephonyManager;
+    // Direct executor
+    private Executor mBackgroundExecutor = Runnable::run;
     private TestableLooper mTestableLooper;
     private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private TestableContext mSpiedContext;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        TestableContext context = spy(mContext);
+        mSpiedContext = spy(mContext);
         when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true);
-        when(context.getPackageManager()).thenReturn(mPackageManager);
+        when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
         doAnswer(invocation -> {
             IBiometricEnabledOnKeyguardCallback callback = invocation.getArgument(0);
             callback.onChanged(BiometricSourceType.FACE, true /* enabled */,
@@ -161,19 +165,20 @@
         when(mStrongAuthTracker
                 .isUnlockingWithBiometricAllowed(anyBoolean() /* isStrongBiometric */))
                 .thenReturn(true);
-        context.addMockSystemService(TrustManager.class, mTrustManager);
-        context.addMockSystemService(FingerprintManager.class, mFingerprintManager);
-        context.addMockSystemService(BiometricManager.class, mBiometricManager);
-        context.addMockSystemService(FaceManager.class, mFaceManager);
-        context.addMockSystemService(UserManager.class, mUserManager);
-        context.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
-        context.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
+        mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager);
+        mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
+        mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager);
+        mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager);
+        mSpiedContext.addMockSystemService(UserManager.class, mUserManager);
+        mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
+        mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager);
+        mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
 
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
-        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(context);
+        mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext);
     }
 
     @After
@@ -192,6 +197,22 @@
     }
 
     @Test
+    public void testSimStateInitialized() {
+        final int subId = 3;
+        final int state = TelephonyManager.SIM_STATE_ABSENT;
+
+        when(mTelephonyManager.getActiveModemCount()).thenReturn(1);
+        when(mTelephonyManager.getSimState(anyInt())).thenReturn(state);
+        when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[] { subId });
+
+        KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext);
+
+        mTestableLooper.processAllMessages();
+
+        assertThat(testKUM.getSimState(subId)).isEqualTo(state);
+    }
+
+    @Test
     public void testIgnoresSimStateCallback_rebroadcast() {
         Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 9e18e61..3ef693a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -85,6 +85,7 @@
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -183,6 +184,8 @@
     private DumpManager mDumpManager;
     @Mock
     private LockscreenLockIconController mLockIconController;
+    @Mock
+    private NotificationShadeWindowView mNotificationShadeWindowView;
 
     private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
     private BubbleData mBubbleData;
@@ -219,8 +222,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardBypassController, mColorExtractor,
                 mDumpManager);
-        mNotificationShadeWindowController.setNotificationShadeView(
-                mSuperStatusBarViewFactory.getNotificationShadeWindowView());
+        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
         // Need notifications for bubbles
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index 49236e0..8e6fc8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -79,6 +79,7 @@
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -134,6 +135,8 @@
     private KeyguardBypassController mKeyguardBypassController;
     @Mock
     private FloatingContentCoordinator mFloatingContentCoordinator;
+    @Mock
+    private NotificationShadeWindowView mNotificationShadeWindowView;
 
     private SysUiState mSysUiState = new SysUiState();
 
@@ -206,8 +209,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardBypassController, mColorExtractor,
                 mDumpManager);
-        mNotificationShadeWindowController.setNotificationShadeView(
-                mSuperStatusBarViewFactory.getNotificationShadeWindowView());
+        mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
         // Need notifications for bubbles
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt
new file mode 100644
index 0000000..628c06a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.systemui.settings
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
[email protected]
+class CurrentUserContextTrackerTest : SysuiTestCase() {
+
+    private lateinit var tracker: CurrentUserContextTracker
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        allowTestableLooperAsMainThread()
+
+        // wrap Context so that tests don't throw for missing package errors
+        val wrapped = object : ContextWrapper(context) {
+            override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+                val mockContext = mock(Context::class.java)
+                `when`(mockContext.user).thenReturn(user)
+                `when`(mockContext.userId).thenReturn(user.identifier)
+                return mockContext
+            }
+        }
+
+        tracker = CurrentUserContextTracker(wrapped, broadcastDispatcher)
+        tracker.initialize()
+    }
+
+    @Test
+    fun testContextExistsAfterInit_noCrash() {
+        tracker.currentUserContext
+    }
+
+    @Test
+    fun testUserContextIsCorrectAfterUserSwitch() {
+        // We always start out with system ui test
+        assertTrue("Starting userId should be 0", tracker.currentUserContext.userId == 0)
+
+        // WHEN user changes
+        tracker.handleUserSwitched(1)
+
+        // THEN user context should have the correct userId
+        assertTrue("User has changed to userId 1, the context should reflect that",
+                tracker.currentUserContext.userId == 1)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    @Test(expected = IllegalStateException::class)
+    fun testContextTrackerThrowsExceptionWhenNotInitialized() {
+        // GIVEN an uninitialized CurrentUserContextTracker
+        val userTracker = CurrentUserContextTracker(context, broadcastDispatcher)
+
+        // WHEN client asks for a context
+        val userContext = userTracker.currentUserContext
+
+        // THEN an exception is thrown
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
index 61388b6..6db8685 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java
@@ -48,9 +48,9 @@
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Person;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
@@ -91,6 +91,7 @@
 import org.mockito.Answers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.mockito.stubbing.Answer;
@@ -147,14 +148,15 @@
     private ShadeController mShadeController;
     @Mock
     private ConversationIconFactory mIconFactory;
-    @Mock
-    private Context mUserContext;
     @Mock(answer = Answers.RETURNS_SELF)
     private PriorityOnboardingDialogController.Builder mBuilder;
     private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder;
+    @Mock
+    private Notification.BubbleMetadata mBubbleMetadata;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mTestableLooper = TestableLooper.get(this);
 
         mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
@@ -228,6 +230,11 @@
         when(mMockINotificationManager.getConversationNotificationChannel(anyString(), anyInt(),
                 anyString(), eq(TEST_CHANNEL), eq(false), eq(CONVERSATION_ID)))
                 .thenReturn(mConversationChannel);
+
+        when(mMockINotificationManager.getConsolidatedNotificationPolicy())
+                .thenReturn(mock(NotificationManager.Policy.class));
+
+        when(mBuilder.build()).thenReturn(mock(PriorityOnboardingDialogController.class));
     }
 
     @Test
@@ -240,10 +247,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
@@ -261,10 +269,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
@@ -283,7 +292,8 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
-                null,
+    mBubbleMetadata,
+    null,
                 null,
                 null,
                 true);
@@ -308,10 +318,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
@@ -331,10 +342,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
@@ -353,10 +365,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
@@ -382,10 +395,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 entry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
@@ -404,13 +418,14 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mConversationChannel, c);
                     latch.countDown();
                 },
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -430,10 +445,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
@@ -451,13 +467,14 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 (View v, NotificationChannel c, int appUid) -> {
                     assertEquals(mNotificationChannel, c);
                     latch.countDown();
                 },
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 false);
         final View settingsButton = mNotificationInfo.findViewById(R.id.info);
@@ -476,10 +493,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.silence);
@@ -501,10 +519,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
@@ -529,10 +548,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
         View view = mNotificationInfo.findViewById(R.id.default_behavior);
@@ -556,10 +576,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -596,10 +617,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -635,10 +657,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -675,10 +698,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -709,10 +733,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -741,10 +766,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -774,10 +800,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -807,10 +834,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -839,10 +867,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -870,10 +899,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -892,10 +922,11 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
+                mContext,
                 mBuilderProvider,
                 true);
 
@@ -910,13 +941,12 @@
 
         // GIVEN the priority onboarding screen is present
         PriorityOnboardingDialogController.Builder b =
-                new PriorityOnboardingDialogController.Builder();
+                mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF);
         PriorityOnboardingDialogController controller =
                 mock(PriorityOnboardingDialogController.class);
         when(b.build()).thenReturn(controller);
 
         // GIVEN the user is changing conversation settings
-        when(mBuilderProvider.get()).thenReturn(b);
         mNotificationInfo.bindNotification(
                 mShortcutManager,
                 mMockPackageManager,
@@ -925,11 +955,12 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
-                mBuilderProvider,
+                mContext,
+                () -> b,
                 true);
 
         // WHEN user clicks "priority"
@@ -945,12 +976,11 @@
         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true);
 
         PriorityOnboardingDialogController.Builder b =
-                new PriorityOnboardingDialogController.Builder();
+                mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF);
         PriorityOnboardingDialogController controller =
                 mock(PriorityOnboardingDialogController.class);
         when(b.build()).thenReturn(controller);
 
-        when(mBuilderProvider.get()).thenReturn(b);
         mNotificationInfo.bindNotification(
                 mShortcutManager,
                 mMockPackageManager,
@@ -959,11 +989,12 @@
                 TEST_PACKAGE_NAME,
                 mNotificationChannel,
                 mEntry,
+                mBubbleMetadata,
                 null,
                 null,
                 mIconFactory,
-                mUserContext,
-                mBuilderProvider,
+                mContext,
+                () -> b,
                 true);
 
         // WHEN user clicks "priority"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
index 2b091f2..210744e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt
@@ -6,11 +6,18 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.AccessibilityController
+import com.android.systemui.statusbar.policy.FlashlightController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.tuner.TunerService
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import java.util.concurrent.Executor
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -24,6 +31,15 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        // Mocked dependencies
+        mDependency.injectMockDependency(AccessibilityController::class.java)
+        mDependency.injectMockDependency(ActivityStarter::class.java)
+        mDependency.injectMockDependency(AssistManager::class.java)
+        mDependency.injectTestDependency(Executor::class.java, Executor { it.run() })
+        mDependency.injectMockDependency(FlashlightController::class.java)
+        mDependency.injectMockDependency(KeyguardStateController::class.java)
+        mDependency.injectMockDependency(TunerService::class.java)
+
         mKeyguardBottomArea = LayoutInflater.from(mContext).inflate(
                 R.layout.keyguard_bottom_area, null, false) as KeyguardBottomAreaView
         mKeyguardBottomArea.setStatusBar(mStatusBar)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
index e052ae2..0a041e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -37,6 +36,7 @@
 import android.os.Handler;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
@@ -64,6 +64,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
+import org.mockito.stubbing.Answer;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -94,9 +95,11 @@
     private Handler mHandler;
     @Mock
     private KeyguardSecurityModel mKeyguardSecurityModel;
+    @Mock
+    private ViewGroup mRootView;
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
-    private ViewGroup mRootView;
+    private Integer mRootVisibility = View.INVISIBLE;
     private KeyguardBouncer mBouncer;
 
     @Before
@@ -105,6 +108,11 @@
         mDependency.injectTestDependency(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor);
         mDependency.injectTestDependency(KeyguardSecurityModel.class, mKeyguardSecurityModel);
         mDependency.injectMockDependency(KeyguardStateController.class);
+        when(mRootView.getVisibility()).thenAnswer((Answer<Integer>) invocation -> mRootVisibility);
+        doAnswer(invocation -> {
+            mRootVisibility = invocation.getArgument(0);
+            return null;
+        }).when(mRootView).setVisibility(anyInt());
         when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
                 .thenReturn(KeyguardSecurityModel.SecurityMode.None);
         DejankUtils.setImmediate(true);
@@ -117,10 +125,8 @@
                 mKeyguardBypassController, mHandler) {
             @Override
             protected void inflateView() {
-                super.inflateView();
                 mKeyguardView = mKeyguardHostView;
-                mRoot = spy(mRoot);
-                mRootView = mRoot;
+                mRoot = mRootView;
             }
         };
     }
@@ -212,8 +218,10 @@
         verify(mExpansionCallback).onFullyShown();
 
         verify(mExpansionCallback, never()).onStartingToHide();
+        verify(mKeyguardHostView, never()).onStartingToHide();
         mBouncer.setExpansion(0.9f);
         verify(mExpansionCallback).onStartingToHide();
+        verify(mKeyguardHostView).onStartingToHide();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
index 962d773..aef454f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
@@ -32,6 +32,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Instrumentation;
@@ -222,7 +223,7 @@
 
         ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackArg =
             ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
-        Mockito.verify(mMockCm, atLeastOnce())
+        verify(mMockCm, atLeastOnce())
             .registerDefaultNetworkCallback(callbackArg.capture(), isA(Handler.class));
         mNetworkCallback = callbackArg.getValue();
         assertNotNull(mNetworkCallback);
@@ -384,7 +385,7 @@
     }
 
     protected void verifyHasNoSims(boolean hasNoSimsVisible) {
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims(
                 eq(hasNoSimsVisible), eq(false));
     }
 
@@ -395,7 +396,7 @@
         ArgumentCaptor<Boolean> dataInArg = ArgumentCaptor.forClass(Boolean.class);
         ArgumentCaptor<Boolean> dataOutArg = ArgumentCaptor.forClass(Boolean.class);
 
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                     any(),
                     iconArg.capture(),
                     anyInt(),
@@ -429,7 +430,7 @@
         ArgumentCaptor<Integer> typeIconArg = ArgumentCaptor.forClass(Integer.class);
 
         // TODO: Verify all fields.
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                 iconArg.capture(),
                 any(),
                 typeIconArg.capture(),
@@ -475,7 +476,7 @@
         ArgumentCaptor<CharSequence> typeContentDescriptionHtmlArg =
                 ArgumentCaptor.forClass(CharSequence.class);
 
-        Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
+        verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators(
                 iconArg.capture(),
                 qsIconArg.capture(),
                 typeIconArg.capture(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
index 9c250c5..988e022 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java
@@ -2,12 +2,14 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.Intent;
+import android.net.ConnectivityManager;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
@@ -171,6 +173,32 @@
         verifyLastWifiIcon(true, WifiIcons.WIFI_SIGNAL_STRENGTH[1][testLevel]);
     }
 
+    @Test
+    public void testFetchInitialData() {
+        mNetworkController.mWifiSignalController.fetchInitialState();
+        Mockito.verify(mMockWm).getWifiState();
+        Mockito.verify(mMockCm).getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+    }
+
+    @Test
+    public void testFetchInitialData_correctValues() {
+        String testSsid = "TEST";
+
+        when(mMockWm.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED);
+        NetworkInfo networkInfo = mock(NetworkInfo.class);
+        when(networkInfo.isConnected()).thenReturn(true);
+        when(mMockCm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)).thenReturn(networkInfo);
+        WifiInfo wifiInfo = mock(WifiInfo.class);
+        when(wifiInfo.getSSID()).thenReturn(testSsid);
+        when(mMockWm.getConnectionInfo()).thenReturn(wifiInfo);
+
+        mNetworkController.mWifiSignalController.fetchInitialState();
+
+        assertTrue(mNetworkController.mWifiSignalController.mCurrentState.enabled);
+        assertTrue(mNetworkController.mWifiSignalController.mCurrentState.connected);
+        assertEquals(testSsid, mNetworkController.mWifiSignalController.mCurrentState.ssid);
+    }
+
     protected void setWifiActivity(int activity) {
         // TODO: Not this, because this variable probably isn't sticking around.
         mNetworkController.mWifiSignalController.setActivity(activity);
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index f21f0e7..1a72cf0 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2732,7 +2732,7 @@
     public void onShellCommand(FileDescriptor in, FileDescriptor out,
             FileDescriptor err, String[] args, ShellCallback callback,
             ResultReceiver resultReceiver) {
-        new AccessibilityShellCommand(this).exec(this, in, out, err, args,
+        new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args,
                 callback, resultReceiver);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
index 20a11bd..b36626f 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 import android.app.ActivityManager;
+import android.os.Binder;
+import android.os.Process;
 import android.os.ShellCommand;
 import android.os.UserHandle;
 
@@ -28,9 +30,12 @@
  */
 final class AccessibilityShellCommand extends ShellCommand {
     final @NonNull AccessibilityManagerService mService;
+    final @NonNull SystemActionPerformer mSystemActionPerformer;
 
-    AccessibilityShellCommand(@NonNull AccessibilityManagerService service) {
+    AccessibilityShellCommand(@NonNull AccessibilityManagerService service,
+            @NonNull SystemActionPerformer systemActionPerformer) {
         mService = service;
+        mSystemActionPerformer = systemActionPerformer;
     }
 
     @Override
@@ -45,6 +50,9 @@
             case "set-bind-instant-service-allowed": {
                 return runSetBindInstantServiceAllowed();
             }
+            case "call-system-action": {
+                return runCallSystemAction();
+            }
         }
         return -1;
     }
@@ -74,6 +82,22 @@
         return 0;
     }
 
+    private int runCallSystemAction() {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid != Process.ROOT_UID
+                && callingUid != Process.SYSTEM_UID
+                && callingUid != Process.SHELL_UID) {
+            return -1;
+        }
+        final String option = getNextArg();
+        if (option != null) {
+            int actionId = Integer.parseInt(option);
+            mSystemActionPerformer.performSystemAction(actionId);
+            return 0;
+        }
+        return -1;
+    }
+
     private Integer parseUserId() {
         final String option = getNextOption();
         if (option != null) {
@@ -97,5 +121,7 @@
         pw.println("    Set whether binding to services provided by instant apps is allowed.");
         pw.println("  get-bind-instant-service-allowed [--user <USER_ID>]");
         pw.println("    Get whether binding to services provided by instant apps is allowed.");
+        pw.println("  call-system-action <ACTION_ID>");
+        pw.println("    Calls the system action with the given action id.");
     }
 }
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index ef8d524..b9e3050 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -301,7 +301,11 @@
                     return lockScreen();
                 case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT:
                     return takeScreenshot();
+                case AccessibilityService.GLOBAL_ACTION_KEYCODE_HEADSETHOOK :
+                    sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HEADSETHOOK);
+                    return true;
                 default:
+                    Slog.e(TAG, "Invalid action id: " + actionId);
                     return false;
             }
         } finally {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 9d1ad42..b27c5d5 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -717,10 +717,11 @@
             Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer =
                     mAssistReceiver.newAutofillRequestLocked(/*isInlineRequest=*/ true);
             if (inlineSuggestionsRequestConsumer != null) {
+                final AutofillId focusedId = mCurrentViewId;
                 remoteRenderService.getInlineSuggestionsRendererInfo(
                         new RemoteCallback((extras) -> {
                             mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
-                                    mCurrentViewId, inlineSuggestionsRequestConsumer, extras);
+                                    focusedId, inlineSuggestionsRequestConsumer, extras);
                         }
                 ));
             }
@@ -2786,6 +2787,12 @@
      */
     private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response,
             @Nullable String filterText) {
+        if (mCurrentViewId == null) {
+            Log.w(TAG, "requestShowInlineSuggestionsLocked(): no view currently focused");
+            return false;
+        }
+        final AutofillId focusedId = mCurrentViewId;
+
         final Optional<InlineSuggestionsRequest> inlineSuggestionsRequest =
                 mInlineSessionController.getInlineSuggestionsRequestLocked();
         if (!inlineSuggestionsRequest.isPresent()) {
@@ -2800,17 +2807,17 @@
             return false;
         }
 
-        final ViewState currentView = mViewStates.get(mCurrentViewId);
+        final ViewState currentView = mViewStates.get(focusedId);
         if ((currentView.getState() & ViewState.STATE_INLINE_DISABLED) != 0) {
             response.getDatasets().clear();
         }
         InlineSuggestionsResponse inlineSuggestionsResponse =
                 InlineSuggestionFactory.createInlineSuggestionsResponse(
-                        inlineSuggestionsRequest.get(), response, filterText, mCurrentViewId,
+                        inlineSuggestionsRequest.get(), response, filterText, focusedId,
                         this, () -> {
                             synchronized (mLock) {
                                 mInlineSessionController.hideInlineSuggestionsUiLocked(
-                                        mCurrentViewId);
+                                        focusedId);
                             }
                         }, remoteRenderService);
         if (inlineSuggestionsResponse == null) {
@@ -2818,7 +2825,7 @@
             return false;
         }
 
-        return mInlineSessionController.onInlineSuggestionsResponseLocked(mCurrentViewId,
+        return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId,
                 inlineSuggestionsResponse);
     }
 
@@ -3107,19 +3114,19 @@
                 remoteService.getComponentName().getPackageName());
         mAugmentedRequestsLogs.add(log);
 
-        final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId);
+        final AutofillId focusedId = mCurrentViewId;
 
         final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill =
                 (inlineSuggestionsRequest) -> {
                     remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName,
-                            focusedId,
+                            AutofillId.withoutSession(focusedId),
                             currentValue, inlineSuggestionsRequest,
                             /*inlineSuggestionsCallback=*/
                             response -> {
                                 synchronized (mLock) {
                                     return mInlineSessionController
                                             .onInlineSuggestionsResponseLocked(
-                                            mCurrentViewId, response);
+                                                    focusedId, response);
                                 }
                             },
                             /*onErrorCallback=*/ () -> {
@@ -3144,7 +3151,7 @@
             remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback(
                     (extras) -> {
                         mInlineSessionController.onCreateInlineSuggestionsRequestLocked(
-                                mCurrentViewId, /*requestConsumer=*/ requestAugmentedAutofill,
+                                focusedId, /*requestConsumer=*/ requestAugmentedAutofill,
                                 extras);
                     }, mHandler));
         } else {
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index c223def..81de29c 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -480,6 +480,12 @@
     public abstract void pruneInstantApps();
 
     /**
+     * Prunes the cache of the APKs in the given APEXes.
+     * @param apexPackages The list of APEX packages that may contain APK-in-APEX.
+     */
+    public abstract void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages);
+
+    /**
      * @return The SetupWizard package name.
      */
     public abstract String getSetupWizardPackageName();
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index ac4a42c..e066d99 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -1034,6 +1034,9 @@
             VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) vib.effect;
             waveform = waveform.resolve(mDefaultVibrationAmplitude);
             scaledEffect = waveform.scale(scale.gamma, scale.maxAmplitude);
+        } else if (vib.effect instanceof VibrationEffect.Composed) {
+            VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.effect;
+            scaledEffect = composed.scale(scale.gamma, scale.maxAmplitude);
         } else {
             Slog.w(TAG, "Unable to apply intensity scaling, unknown VibrationEffect type");
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 8f5fbf7..149e3ba 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -2504,7 +2504,7 @@
 
         IUsageStatsManager usm = IUsageStatsManager.Stub.asInterface(ServiceManager.getService(
                 Context.USAGE_STATS_SERVICE));
-        boolean isIdle = usm.isAppInactive(packageName, userId);
+        boolean isIdle = usm.isAppInactive(packageName, userId, SHELL_PACKAGE_NAME);
         pw.println("Idle=" + isIdle);
         return 0;
     }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
index fbee6f4..8480197 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java
@@ -26,6 +26,7 @@
 import android.app.trust.IStrongAuthTracker;
 import android.content.Context;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -36,6 +37,7 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker;
 
@@ -57,11 +59,14 @@
     private static final int MSG_STRONG_BIOMETRIC_UNLOCK = 8;
     private static final int MSG_SCHEDULE_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT = 9;
 
-    private static final String STRONG_AUTH_TIMEOUT_ALARM_TAG =
+    @VisibleForTesting
+    protected static final String STRONG_AUTH_TIMEOUT_ALARM_TAG =
             "LockSettingsStrongAuth.timeoutForUser";
-    private static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG =
+    @VisibleForTesting
+    protected static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG =
             "LockSettingsPrimaryAuth.nonStrongBiometricTimeoutForUser";
-    private static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG =
+    @VisibleForTesting
+    protected static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG =
             "LockSettingsPrimaryAuth.nonStrongBiometricIdleTimeoutForUser";
 
     /**
@@ -73,28 +78,71 @@
             4 * 60 * 60 * 1000; // 4h
 
     private final RemoteCallbackList<IStrongAuthTracker> mTrackers = new RemoteCallbackList<>();
-    private final SparseIntArray mStrongAuthForUser = new SparseIntArray();
-    private final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = new SparseBooleanArray();
-    private final ArrayMap<Integer, StrongAuthTimeoutAlarmListener>
+    @VisibleForTesting
+    protected final SparseIntArray mStrongAuthForUser = new SparseIntArray();
+    @VisibleForTesting
+    protected final SparseBooleanArray mIsNonStrongBiometricAllowedForUser =
+            new SparseBooleanArray();
+    @VisibleForTesting
+    protected final ArrayMap<Integer, StrongAuthTimeoutAlarmListener>
             mStrongAuthTimeoutAlarmListenerForUser = new ArrayMap<>();
     // Track non-strong biometric timeout
-    private final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener>
+    @VisibleForTesting
+    protected final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener>
             mNonStrongBiometricTimeoutAlarmListener = new ArrayMap<>();
     // Track non-strong biometric idle timeout
-    private final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener>
+    @VisibleForTesting
+    protected final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener>
             mNonStrongBiometricIdleTimeoutAlarmListener = new ArrayMap<>();
 
     private final int mDefaultStrongAuthFlags;
     private final boolean mDefaultIsNonStrongBiometricAllowed = true;
 
     private final Context mContext;
-
-    private AlarmManager mAlarmManager;
+    private final Injector mInjector;
+    private final AlarmManager mAlarmManager;
 
     public LockSettingsStrongAuth(Context context) {
+        this(context, new Injector());
+    }
+
+    @VisibleForTesting
+    protected LockSettingsStrongAuth(Context context, Injector injector) {
         mContext = context;
-        mDefaultStrongAuthFlags = StrongAuthTracker.getDefaultFlags(context);
-        mAlarmManager = context.getSystemService(AlarmManager.class);
+        mInjector = injector;
+        mDefaultStrongAuthFlags = mInjector.getDefaultStrongAuthFlags(context);
+        mAlarmManager = mInjector.getAlarmManager(context);
+    }
+
+    /**
+     * Class for injecting dependencies into LockSettingsStrongAuth.
+     */
+    @VisibleForTesting
+    public static class Injector {
+
+        /**
+         * Allows to mock AlarmManager for testing.
+         */
+        @VisibleForTesting
+        public AlarmManager getAlarmManager(Context context) {
+            return context.getSystemService(AlarmManager.class);
+        }
+
+        /**
+         * Allows to get different default StrongAuthFlags for testing.
+         */
+        @VisibleForTesting
+        public int getDefaultStrongAuthFlags(Context context) {
+            return StrongAuthTracker.getDefaultFlags(context);
+        }
+
+        /**
+         * Allows to get different triggerAtMillis values when setting alarms for testing.
+         */
+        @VisibleForTesting
+        public long getNextAlarmTimeMs(long timeout) {
+            return SystemClock.elapsedRealtime() + timeout;
+        }
     }
 
     private void handleAddStrongAuthTracker(IStrongAuthTracker tracker) {
@@ -186,7 +234,8 @@
     private void handleScheduleStrongAuthTimeout(int userId) {
         final DevicePolicyManager dpm =
                 (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
-        long when = SystemClock.elapsedRealtime() + dpm.getRequiredStrongAuthTimeout(null, userId);
+        long nextAlarmTime =
+                mInjector.getNextAlarmTimeMs(dpm.getRequiredStrongAuthTimeout(null, userId));
         // cancel current alarm listener for the user (if there was one)
         StrongAuthTimeoutAlarmListener alarm = mStrongAuthTimeoutAlarmListenerForUser.get(userId);
         if (alarm != null) {
@@ -196,8 +245,8 @@
             mStrongAuthTimeoutAlarmListenerForUser.put(userId, alarm);
         }
         // schedule a new alarm listener for the user
-        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, STRONG_AUTH_TIMEOUT_ALARM_TAG,
-                alarm, mHandler);
+        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime,
+                STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm, mHandler);
 
         // cancel current non-strong biometric alarm listener for the user (if there was one)
         cancelNonStrongBiometricAlarmListener(userId);
@@ -209,7 +258,7 @@
 
     private void handleScheduleNonStrongBiometricTimeout(int userId) {
         if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricTimeout for userId=" + userId);
-        long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS;
+        long nextAlarmTime = mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS);
         NonStrongBiometricTimeoutAlarmListener alarm = mNonStrongBiometricTimeoutAlarmListener
                 .get(userId);
         if (alarm != null) {
@@ -226,7 +275,7 @@
             alarm = new NonStrongBiometricTimeoutAlarmListener(userId);
             mNonStrongBiometricTimeoutAlarmListener.put(userId, alarm);
             // schedule a new alarm listener for the user
-            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when,
+            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime,
                     NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm, mHandler);
         }
 
@@ -268,7 +317,8 @@
         }
     }
 
-    private void setIsNonStrongBiometricAllowed(boolean allowed, int userId) {
+    @VisibleForTesting
+    protected void setIsNonStrongBiometricAllowed(boolean allowed, int userId) {
         if (DEBUG) {
             Slog.d(TAG, "setIsNonStrongBiometricAllowed for allowed=" + allowed
                     + ", userId=" + userId);
@@ -302,7 +352,8 @@
 
     private void handleScheduleNonStrongBiometricIdleTimeout(int userId) {
         if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricIdleTimeout for userId=" + userId);
-        long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS;
+        long nextAlarmTime =
+                mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS);
         // cancel current alarm listener for the user (if there was one)
         NonStrongBiometricIdleTimeoutAlarmListener alarm =
                 mNonStrongBiometricIdleTimeoutAlarmListener.get(userId);
@@ -315,7 +366,7 @@
         }
         // schedule a new alarm listener for the user
         if (DEBUG) Slog.d(TAG, "Schedule a new alarm for non-strong biometric idle timeout");
-        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when,
+        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime,
                 NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm, mHandler);
     }
 
@@ -435,7 +486,8 @@
     /**
      * Alarm of fallback timeout for primary auth
      */
-    private class StrongAuthTimeoutAlarmListener implements OnAlarmListener {
+    @VisibleForTesting
+    protected class StrongAuthTimeoutAlarmListener implements OnAlarmListener {
 
         private final int mUserId;
 
@@ -452,7 +504,8 @@
     /**
      * Alarm of fallback timeout for non-strong biometric (i.e. weak or convenience)
      */
-    private class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener {
+    @VisibleForTesting
+    protected class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener {
 
         private final int mUserId;
 
@@ -469,7 +522,8 @@
     /**
      * Alarm of idle timeout for non-strong biometric (i.e. weak or convenience biometric)
      */
-    private class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener {
+    @VisibleForTesting
+    protected class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener {
 
         private final int mUserId;
 
@@ -484,7 +538,8 @@
         }
     }
 
-    private final Handler mHandler = new Handler() {
+    @VisibleForTesting
+    protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
index 28f8380..3cf22c8 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java
@@ -215,7 +215,6 @@
                 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
                 .setDescription(mContext.getResources().getText(
                         R.string.bluetooth_a2dp_audio_route_name).toString())
-                //TODO: Set type correctly (BLUETOOTH_A2DP or HEARING_AID)
                 .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)
                 .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
                 .build();
@@ -236,6 +235,8 @@
         // Update volume when the connection state is changed.
         MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route)
                 .setConnectionState(state);
+        builder.setType(btRoute.connectedProfiles.get(BluetoothProfile.HEARING_AID, false)
+                ? MediaRoute2Info.TYPE_HEARING_AID : MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
 
         if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) {
             int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 0d89997..d8bf9ed 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.media;
 
+import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE;
+import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
 import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE;
 import static android.media.MediaRouter2Utils.getOriginalId;
 import static android.media.MediaRouter2Utils.getProviderId;
@@ -247,6 +249,22 @@
         }
     }
 
+    public void notifySessionHintsForCreatingSession(IMediaRouter2 router,
+            long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) {
+        Objects.requireNonNull(router, "router must not be null");
+        Objects.requireNonNull(route, "route must not be null");
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                notifySessionHintsForCreatingSessionLocked(uniqueRequestId,
+                        router, route, sessionHints);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     public void selectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
             MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
@@ -265,7 +283,6 @@
         }
     }
 
-
     public void deselectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId,
             MediaRoute2Info route) {
         Objects.requireNonNull(router, "router must not be null");
@@ -634,12 +651,30 @@
 
         long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, /* managerRecord= */ null, route,
+                        uniqueRequestId, routerRecord, route,
                         sessionHints));
     }
 
+    private void notifySessionHintsForCreatingSessionLocked(long uniqueRequestId,
+            @NonNull IMediaRouter2 router,
+            @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+        final IBinder binder = router.asBinder();
+        final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+        if (routerRecord == null) {
+            Slog.w(TAG, "requestCreateSessionWithRouter2ByManagerRequestLocked: "
+                    + "Ignoring unknown router.");
+            return;
+        }
+
+        routerRecord.mUserRecord.mHandler.sendMessage(
+                obtainMessage(UserHandler::requestCreateSessionWithManagerOnHandler,
+                        routerRecord.mUserRecord.mHandler,
+                        uniqueRequestId, routerRecord, route, sessionHints));
+    }
+
     private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router,
             @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
         final IBinder binder = router.asBinder();
@@ -826,12 +861,13 @@
         }
 
         long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId);
-        //TODO(b/152851868): Use MediaRouter2's OnCreateSessionListener to send session hints.
+
+        // Before requesting to the provider, get session hints from the media router.
+        // As a return, media router will request to create a session.
         routerRecord.mUserRecord.mHandler.sendMessage(
-                obtainMessage(UserHandler::requestCreateSessionOnHandler,
+                obtainMessage(UserHandler::getSessionHintsForCreatingSessionOnHandler,
                         routerRecord.mUserRecord.mHandler,
-                        uniqueRequestId, routerRecord, managerRecord, route,
-                        /* sessionHints= */ null));
+                        uniqueRequestId, routerRecord, managerRecord, route));
     }
 
     private void selectRouteWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager,
@@ -1149,7 +1185,6 @@
                     this, provider, uniqueRequestId, sessionInfo));
         }
 
-
         @Override
         public void onSessionUpdated(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo) {
@@ -1267,8 +1302,26 @@
             return -1;
         }
 
-        private void requestCreateSessionOnHandler(long uniqueRequestId,
-                @NonNull RouterRecord routerRecord, @Nullable ManagerRecord managerRecord,
+        private void getSessionHintsForCreatingSessionOnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord,
+                @NonNull MediaRoute2Info route) {
+            SessionCreationRequest request =
+                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord);
+            mSessionCreationRequests.add(request);
+
+            try {
+                routerRecord.mRouter.getSessionHintsForCreatingSession(uniqueRequestId, route);
+            } catch (RemoteException ex) {
+                Slog.w(TAG, "requestGetSessionHintsOnHandler: "
+                        + "Failed to request. Router probably died.");
+                mSessionCreationRequests.remove(request);
+                notifyRequestFailedToManager(managerRecord.mManager,
+                        toOriginalRequestId(uniqueRequestId), REASON_UNKNOWN_ERROR);
+            }
+        }
+
+        private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord,
                 @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
 
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
@@ -1281,13 +1334,50 @@
             }
 
             SessionCreationRequest request =
-                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord);
+                    new SessionCreationRequest(routerRecord, uniqueRequestId, route, null);
             mSessionCreationRequests.add(request);
 
             provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName,
                     route.getOriginalId(), sessionHints);
         }
 
+        private void requestCreateSessionWithManagerOnHandler(long uniqueRequestId,
+                @NonNull RouterRecord routerRecord,
+                @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) {
+            SessionCreationRequest matchingRequest = null;
+            for (SessionCreationRequest request : mSessionCreationRequests) {
+                if (request.mUniqueRequestId == uniqueRequestId) {
+                    matchingRequest = request;
+                    break;
+                }
+            }
+            if (matchingRequest == null) {
+                Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: "
+                        + "Ignoring an unknown request.");
+                return;
+            }
+
+            if (!TextUtils.equals(matchingRequest.mRoute.getId(), route.getId())) {
+                Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: "
+                        + "The given route is different from the requested route.");
+                return;
+            }
+
+            final MediaRoute2Provider provider = findProvider(route.getProviderId());
+            if (provider == null) {
+                Slog.w(TAG, "Ignoring session creation request since no provider found for"
+                        + " given route=" + route);
+
+                mSessionCreationRequests.remove(matchingRequest);
+                notifyRequestFailedToManager(matchingRequest.mRequestedManagerRecord.mManager,
+                        toOriginalRequestId(uniqueRequestId), REASON_ROUTE_NOT_AVAILABLE);
+                return;
+            }
+
+            provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName,
+                    route.getOriginalId(), sessionHints);
+        }
+
         // routerRecord can be null if the session is system's or RCN.
         private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord,
                 @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) {
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index d6bf9fb..bf2cc5e 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -487,6 +487,14 @@
 
     // Binder call
     @Override
+    public void notifySessionHintsForCreatingSession(IMediaRouter2 router,
+            long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) {
+        mService2.notifySessionHintsForCreatingSession(router,
+                uniqueRequestId, route, sessionHints);
+    }
+
+    // Binder call
+    @Override
     public void selectRouteWithRouter2(IMediaRouter2 router, String sessionId,
             MediaRoute2Info route) {
         mService2.selectRouteWithRouter2(router, sessionId, route);
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index e5867e7..476c9f8 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -165,6 +165,10 @@
         mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance(mContext);
         mAudioPlayerStateMonitor.registerListener(
                 (config, isRemoved) -> {
+                    if (DEBUG) {
+                        Log.d(TAG, "Audio playback is changed, config=" + config
+                                + ", removed=" + isRemoved);
+                    }
                     if (config.getPlayerType()
                             == AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL) {
                         return;
@@ -1993,7 +1997,7 @@
                     FullUserRecord user = getFullUserRecordLocked(record.getUserId());
                     if (record != null && user != null) {
                         record.setSessionPolicies(policies);
-                        user.mPriorityStack.updateMediaButtonSessionIfNeeded();
+                        user.mPriorityStack.updateMediaButtonSessionBySessionPolicyChange(record);
                     }
                 }
             } finally {
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index bd98b9c..402355a 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -16,6 +16,8 @@
 
 package com.android.server.media;
 
+import static com.android.server.media.SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION;
+
 import android.media.Session2Token;
 import android.media.session.MediaSession;
 import android.os.Debug;
@@ -102,6 +104,7 @@
             // When the media button session is removed, nullify the media button session and do not
             // search for the alternative media session within the app. It's because the alternative
             // media session might be a dummy which isn't able to handle the media key events.
+            // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
             updateMediaButtonSession(null);
         }
         clearCache(record.getUserId());
@@ -158,7 +161,7 @@
                     findMediaButtonSession(mMediaButtonSession.getUid());
             if (newMediaButtonSession != mMediaButtonSession
                     && (newMediaButtonSession.getSessionPolicies()
-                    & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
+                            & SESSION_POLICY_IGNORE_BUTTON_SESSION) == 0) {
                 // Check if the policy states that this session should not be updated as a media
                 // button session.
                 updateMediaButtonSession(newMediaButtonSession);
@@ -189,19 +192,43 @@
         }
         IntArray audioPlaybackUids = mAudioPlayerStateMonitor.getSortedAudioPlaybackClientUids();
         for (int i = 0; i < audioPlaybackUids.size(); i++) {
-            MediaSessionRecordImpl mediaButtonSession =
-                    findMediaButtonSession(audioPlaybackUids.get(i));
-            if (mediaButtonSession == null) continue;
-            boolean ignoreButtonSession = (mediaButtonSession.getSessionPolicies()
-                    & SessionPolicyProvider.SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
-            if (mediaButtonSession == mMediaButtonSession && ignoreButtonSession) {
+            int audioPlaybackUid = audioPlaybackUids.get(i);
+            MediaSessionRecordImpl mediaButtonSession = findMediaButtonSession(audioPlaybackUid);
+            if (mediaButtonSession == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "updateMediaButtonSessionIfNeeded, skipping uid="
+                            + audioPlaybackUid);
+                }
+                // Ignore if the lastly played app isn't a media app (i.e. has no media session)
+                continue;
+            }
+            boolean ignoreButtonSession =
+                    (mediaButtonSession.getSessionPolicies()
+                            & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0;
+            if (DEBUG) {
+                Log.d(TAG, "updateMediaButtonSessionIfNeeded, checking uid=" + audioPlaybackUid
+                        + ", mediaButtonSession=" + mediaButtonSession
+                        + ", ignoreButtonSession=" + ignoreButtonSession);
+            }
+            if (!ignoreButtonSession) {
+                mAudioPlayerStateMonitor.cleanUpAudioPlaybackUids(mediaButtonSession.getUid());
+                if (mediaButtonSession != mMediaButtonSession) {
+                    updateMediaButtonSession(mediaButtonSession);
+                }
+                return;
+            }
+        }
+    }
+
+    // TODO: Remove this and make updateMediaButtonSessionIfNeeded() to also cover this case.
+    public void updateMediaButtonSessionBySessionPolicyChange(MediaSessionRecord record) {
+        if ((record.getSessionPolicies() & SESSION_POLICY_IGNORE_BUTTON_SESSION) != 0) {
+            if (record == mMediaButtonSession) {
+                // TODO(b/154456172): Make this decision unaltered by non-media app's playback.
                 updateMediaButtonSession(null);
-                return;
             }
-            if (mediaButtonSession != mMediaButtonSession && !ignoreButtonSession) {
-                updateMediaButtonSession(mediaButtonSession);
-                return;
-            }
+        } else {
+            updateMediaButtonSessionIfNeeded();
         }
     }
 
@@ -280,7 +307,7 @@
         return mMediaButtonSession;
     }
 
-    private void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
+    public void updateMediaButtonSession(MediaSessionRecordImpl newMediaButtonSession) {
         MediaSessionRecordImpl oldMediaButtonSession = mMediaButtonSession;
         mMediaButtonSession = newMediaButtonSession;
         mOnMediaButtonSessionChangedListener.onMediaButtonSessionChanged(
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 1345e37..41d7fff 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -19,6 +19,11 @@
 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
 import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_DOCK;
+import static android.media.MediaRoute2Info.TYPE_HDMI;
+import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -194,19 +199,27 @@
 
     private void updateDeviceRoute(AudioRoutesInfo newRoutes) {
         int name = R.string.default_audio_route_name;
+        int type = TYPE_BUILTIN_SPEAKER;
         if (newRoutes != null) {
             mCurAudioRoutesInfo.mainType = newRoutes.mainType;
-            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
-                    || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
+                type = TYPE_WIRED_HEADPHONES;
+                name = com.android.internal.R.string.default_audio_route_name_headphones;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+                type = TYPE_WIRED_HEADSET;
                 name = com.android.internal.R.string.default_audio_route_name_headphones;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
+                type = TYPE_DOCK;
                 name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
+                type = TYPE_HDMI;
                 name = com.android.internal.R.string.default_audio_route_name_hdmi;
             } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
+                type = TYPE_USB_DEVICE;
                 name = com.android.internal.R.string.default_audio_route_name_usb;
             }
         }
+
         mDeviceRoute = new MediaRoute2Info.Builder(
                 DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString())
                 .setVolumeHandling(mAudioManager.isVolumeFixed()
@@ -214,8 +227,7 @@
                         : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
                 .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
                 .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
-                //TODO: Guess the exact type using AudioDevice
-                .setType(TYPE_BUILTIN_SPEAKER)
+                .setType(type)
                 .addFeature(FEATURE_LIVE_AUDIO)
                 .addFeature(FEATURE_LIVE_VIDEO)
                 .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 2221644..6b1ef3a 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -150,7 +150,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -2099,11 +2098,13 @@
                             continue;
                         }
 
-                        mResolvedInstructionSets.add(archSubDir.getName());
-                        List<File> oatFiles = Arrays.asList(archSubDir.listFiles());
-                        if (!oatFiles.isEmpty()) {
-                            mResolvedInheritedFiles.addAll(oatFiles);
+                        File[] files = archSubDir.listFiles();
+                        if (files == null || files.length == 0) {
+                            continue;
                         }
+
+                        mResolvedInstructionSets.add(archSubDir.getName());
+                        mResolvedInheritedFiles.addAll(Arrays.asList(files));
                     }
                 }
             }
@@ -2117,7 +2118,8 @@
                     if (!libDir.exists() || !libDir.isDirectory()) {
                         continue;
                     }
-                    final List<File> libDirsToInherit = new LinkedList<>();
+                    final List<String> libDirsToInherit = new ArrayList<>();
+                    final List<File> libFilesToInherit = new ArrayList<>();
                     for (File archSubDir : libDir.listFiles()) {
                         if (!archSubDir.isDirectory()) {
                             continue;
@@ -2129,14 +2131,24 @@
                             Slog.e(TAG, "Skipping linking of native library directory!", e);
                             // shouldn't be possible, but let's avoid inheriting these to be safe
                             libDirsToInherit.clear();
+                            libFilesToInherit.clear();
                             break;
                         }
-                        if (!mResolvedNativeLibPaths.contains(relLibPath)) {
-                            mResolvedNativeLibPaths.add(relLibPath);
+
+                        File[] files = archSubDir.listFiles();
+                        if (files == null || files.length == 0) {
+                            continue;
                         }
-                        libDirsToInherit.addAll(Arrays.asList(archSubDir.listFiles()));
+
+                        libDirsToInherit.add(relLibPath);
+                        libFilesToInherit.addAll(Arrays.asList(files));
                     }
-                    mResolvedInheritedFiles.addAll(libDirsToInherit);
+                    for (String subDir : libDirsToInherit) {
+                        if (!mResolvedNativeLibPaths.contains(subDir)) {
+                            mResolvedNativeLibPaths.add(subDir);
+                        }
+                    }
+                    mResolvedInheritedFiles.addAll(libFilesToInherit);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index f0fee0a..7adafe3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -351,6 +351,7 @@
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.dex.ViewCompiler;
+import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.library.PackageBackwardCompatibility;
@@ -1346,13 +1347,6 @@
                     int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
                     boolean needUpdate = false;
 
-                    if (DEBUG_DOMAIN_VERIFICATION) {
-                        Slog.d(TAG,
-                                "Updating IntentFilterVerificationInfo for package " + packageName
-                                + " verificationId:" + verificationId
-                                + " verified=" + verified);
-                    }
-
                     // In a success case, we promote from undefined or ASK to ALWAYS.  This
                     // supports a flow where the app fails validation but then ships an updated
                     // APK that passes, and therefore deserves to be in ALWAYS.
@@ -17639,65 +17633,120 @@
                 + " Activities needs verification ...");
 
         int count = 0;
-
+        boolean handlesWebUris = false;
+        ArraySet<String> domains = new ArraySet<>();
+        final boolean previouslyVerified;
+        boolean hostSetExpanded = false;
+        boolean needToRunVerify = false;
         synchronized (mLock) {
             // If this is a new install and we see that we've already run verification for this
             // package, we have nothing to do: it means the state was restored from backup.
-            if (!replacing) {
-                IntentFilterVerificationInfo ivi =
-                        mSettings.getIntentFilterVerificationLPr(packageName);
-                if (ivi != null) {
-                    if (DEBUG_DOMAIN_VERIFICATION) {
-                        Slog.i(TAG, "Package " + packageName+ " already verified: status="
-                                + ivi.getStatusString());
-                    }
-                    return;
+            IntentFilterVerificationInfo ivi =
+                    mSettings.getIntentFilterVerificationLPr(packageName);
+            previouslyVerified = (ivi != null);
+            if (!replacing && previouslyVerified) {
+                if (DEBUG_DOMAIN_VERIFICATION) {
+                    Slog.i(TAG, "Package " + packageName + " already verified: status="
+                            + ivi.getStatusString());
                 }
+                return;
             }
 
-            // If any filters need to be verified, then all need to be.
-            boolean needToVerify = false;
+            if (DEBUG_DOMAIN_VERIFICATION) {
+                Slog.i(TAG, "    Previous verified hosts: "
+                        + (ivi == null ? "[none]" : ivi.getDomainsString()));
+            }
+
+            // If any filters need to be verified, then all need to be.  In addition, we need to
+            // know whether an updating app has any web navigation intent filters, to re-
+            // examine handling policy even if not re-verifying.
+            final boolean needsVerification = needsNetworkVerificationLPr(packageName);
             for (ParsedActivity a : activities) {
                 for (ParsedIntentInfo filter : a.getIntents()) {
-                    if (filter.needsVerification()
-                            && needsNetworkVerificationLPr(a.getPackageName())) {
+                    if (filter.handlesWebUris(true)) {
+                        handlesWebUris = true;
+                    }
+                    if (needsVerification && filter.needsVerification()) {
                         if (DEBUG_DOMAIN_VERIFICATION) {
-                            Slog.d(TAG,
-                                    "Intent filter needs verification, so processing all filters");
+                            Slog.d(TAG, "autoVerify requested, processing all filters");
                         }
-                        needToVerify = true;
+                        needToRunVerify = true;
+                        // It's safe to break out here because filter.needsVerification()
+                        // can only be true if filter.handlesWebUris(true) returned true, so
+                        // we've already noted that.
                         break;
                     }
                 }
             }
 
-            if (needToVerify) {
-                final boolean needsVerification = needsNetworkVerificationLPr(packageName);
+            // Compare the new set of recognized hosts if the app is either requesting
+            // autoVerify or has previously used autoVerify but no longer does.
+            if (needToRunVerify || previouslyVerified) {
                 final int verificationId = mIntentFilterVerificationToken++;
                 for (ParsedActivity a : activities) {
                     for (ParsedIntentInfo filter : a.getIntents()) {
                         // Run verification against hosts mentioned in any web-nav intent filter,
                         // even if the filter matches non-web schemes as well
-                        if (needsVerification && filter.handlesWebUris(false)) {
+                        if (filter.handlesWebUris(false /*onlyWebSchemes*/)) {
                             if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
                                     "Verification needed for IntentFilter:" + filter.toString());
                             mIntentFilterVerifier.addOneIntentFilterVerification(
                                     verifierUid, userId, verificationId, filter, packageName);
+                            domains.addAll(filter.getHostsList());
                             count++;
                         }
                     }
                 }
             }
+
+            if (DEBUG_DOMAIN_VERIFICATION) {
+                Slog.i(TAG, "    Update published hosts: " + domains.toString());
+            }
+
+            // If we've previously verified this same host set (or a subset), we can trust that
+            // a current ALWAYS policy is still applicable.  If this is the case, we're done.
+            // (If we aren't in ALWAYS, we want to reverify to allow for apps that had failing
+            // hosts in their intent filters, then pushed a new apk that removed them and now
+            // passes.)
+            //
+            // Cases:
+            //   + still autoVerify (needToRunVerify):
+            //      - preserve current state if all of: unexpanded, in always
+            //      - otherwise rerun as usual (fall through)
+            //   + no longer autoVerify (alreadyVerified && !needToRunVerify)
+            //      - wipe verification history always
+            //      - preserve current state if all of: unexpanded, in always
+            hostSetExpanded = !previouslyVerified
+                    || (ivi != null && !ivi.getDomains().containsAll(domains));
+            final int currentPolicy =
+                    mSettings.getIntentFilterVerificationStatusLPr(packageName, userId);
+            final boolean keepCurState = !hostSetExpanded
+                    && currentPolicy == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
+
+            if (needToRunVerify && keepCurState) {
+                if (DEBUG_DOMAIN_VERIFICATION) {
+                    Slog.i(TAG, "Host set not expanding + ALWAYS -> no need to reverify");
+                }
+                ivi.setDomains(domains);
+                scheduleWriteSettingsLocked();
+                return;
+            } else if (previouslyVerified && !needToRunVerify) {
+                // Prior autoVerify state but not requesting it now.  Clear autoVerify history,
+                // and preserve the always policy iff the host set is not expanding.
+                clearIntentFilterVerificationsLPw(packageName, userId, !keepCurState);
+                return;
+            }
         }
 
-        if (count > 0) {
+        if (needToRunVerify && count > 0) {
+            // app requested autoVerify and has at least one matching intent filter
             if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Starting " + count
                     + " IntentFilter verification" + (count > 1 ? "s" : "")
                     +  " for userId:" + userId);
             mIntentFilterVerifier.startVerifications(userId);
         } else {
             if (DEBUG_DOMAIN_VERIFICATION) {
-                Slog.d(TAG, "No filters or not all autoVerify for " + packageName);
+                Slog.d(TAG, "No web filters or no new host policy for " + packageName);
             }
         }
     }
@@ -18402,7 +18451,7 @@
             if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
                 final SparseBooleanArray changedUsers = new SparseBooleanArray();
                 synchronized (mLock) {
-                    clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL);
+                    clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL, true);
                     clearDefaultBrowserIfNeeded(packageName);
                     mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName);
                     removedAppId = mSettings.removePackageLPw(packageName);
@@ -19495,13 +19544,14 @@
         final int packageCount = mPackages.size();
         for (int i = 0; i < packageCount; i++) {
             AndroidPackage pkg = mPackages.valueAt(i);
-            clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId);
+            clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId, true);
         }
     }
 
     /** This method takes a specific user id as well as UserHandle.USER_ALL. */
     @GuardedBy("mLock")
-    void clearIntentFilterVerificationsLPw(String packageName, int userId) {
+    void clearIntentFilterVerificationsLPw(String packageName, int userId,
+            boolean alsoResetStatus) {
         if (userId == UserHandle.USER_ALL) {
             if (mSettings.removeIntentFilterVerificationLPw(packageName,
                     mUserManager.getUserIds())) {
@@ -19510,7 +19560,8 @@
                 }
             }
         } else {
-            if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) {
+            if (mSettings.removeIntentFilterVerificationLPw(packageName, userId,
+                    alsoResetStatus)) {
                 scheduleWritePackageRestrictionsLocked(userId);
             }
         }
@@ -24182,6 +24233,25 @@
         }
 
         @Override
+        public void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages) {
+            if (mCacheDir == null) {
+                return;
+            }
+
+            final PackageCacher cacher = new PackageCacher(mCacheDir);
+            synchronized (mLock) {
+                for (int i = 0, size = apexPackages.size(); i < size; i++) {
+                    final List<String> apkNames =
+                            mApexManager.getApksInApex(apexPackages.get(i).packageName);
+                    for (int j = 0, apksInApex = apkNames.size(); j < apksInApex; j++) {
+                        final AndroidPackage pkg = getPackage(apkNames.get(j));
+                        cacher.cleanCachedResult(new File(pkg.getCodePath()));
+                    }
+                }
+            }
+        }
+
+        @Override
         public String getSetupWizardPackageName() {
             return mSetupWizardPackage;
         }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index e2da99a..ddeab29 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -1282,7 +1282,8 @@
         return result;
     }
 
-    boolean removeIntentFilterVerificationLPw(String packageName, int userId) {
+    boolean removeIntentFilterVerificationLPw(String packageName, int userId,
+            boolean alsoResetStatus) {
         PackageSetting ps = mPackages.get(packageName);
         if (ps == null) {
             if (DEBUG_DOMAIN_VERIFICATION) {
@@ -1290,14 +1291,17 @@
             }
             return false;
         }
-        ps.clearDomainVerificationStatusForUser(userId);
+        if (alsoResetStatus) {
+            ps.clearDomainVerificationStatusForUser(userId);
+        }
+        ps.setIntentFilterVerificationInfo(null);
         return true;
     }
 
     boolean removeIntentFilterVerificationLPw(String packageName, int[] userIds) {
         boolean result = false;
         for (int userId : userIds) {
-            result |= removeIntentFilterVerificationLPw(packageName, userId);
+            result |= removeIntentFilterVerificationLPw(packageName, userId, true);
         }
         return result;
     }
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java
index d1d9877..1c1e64d 100644
--- a/services/core/java/com/android/server/pm/StagingManager.java
+++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -1226,8 +1226,9 @@
             // APEX checks. For single-package sessions, check if they contain an APEX. For
             // multi-package sessions, find all the child sessions that contain an APEX.
             if (hasApex) {
+                final List<PackageInfo> apexPackages;
                 try {
-                    final List<PackageInfo> apexPackages = submitSessionToApexService(session);
+                    apexPackages = submitSessionToApexService(session);
                     for (int i = 0, size = apexPackages.size(); i < size; i++) {
                         validateApexSignature(apexPackages.get(i));
                     }
@@ -1235,6 +1236,10 @@
                     session.setStagedSessionFailed(e.error, e.getMessage());
                     return;
                 }
+
+                final PackageManagerInternal packageManagerInternal =
+                        LocalServices.getService(PackageManagerInternal.class);
+                packageManagerInternal.pruneCachedApksInApex(apexPackages);
             }
 
             notifyPreRebootVerification_Apex_Complete(session.sessionId);
diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
index e5e1b0b..99c6dd1 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.content.pm.PackageParserCacheHelper;
+import android.os.FileUtils;
 import android.os.Parcel;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -197,4 +198,18 @@
             Slog.w(TAG, "Error saving package cache.", e);
         }
     }
+
+    /**
+     * Delete the cache files for the given {@code packageFile}.
+     */
+    public void cleanCachedResult(@NonNull File packageFile) {
+        final String packageName = packageFile.getName();
+        final File[] files = FileUtils.listFilesOrEmpty(mCacheDir,
+                (dir, name) -> name.startsWith(packageName));
+        for (File file : files) {
+            if (!file.delete()) {
+                Slog.e(TAG, "Unable to clean cache file: " + file);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
index 059861b..701197e 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java
@@ -214,7 +214,7 @@
      * required adjustments.
      */
     @GuardedBy("mLock")
-    private Policy mEffectivePolicy = OFF_POLICY;
+    private Policy mEffectivePolicyRaw = OFF_POLICY;
 
     @IntDef(prefix = {"POLICY_LEVEL_"}, value = {
             POLICY_LEVEL_OFF,
@@ -228,12 +228,8 @@
     static final int POLICY_LEVEL_ADAPTIVE = 1;
     static final int POLICY_LEVEL_FULL = 2;
 
-    /**
-     * Do not access directly; always use {@link #setPolicyLevel}
-     * and {@link #getPolicyLevelLocked}
-     */
     @GuardedBy("mLock")
-    private int mPolicyLevelRaw = POLICY_LEVEL_OFF;
+    private int mPolicyLevel = POLICY_LEVEL_OFF;
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
@@ -338,7 +334,7 @@
     private void maybeNotifyListenersOfPolicyChange() {
         final BatterySaverPolicyListener[] listeners;
         synchronized (mLock) {
-            if (getPolicyLevelLocked() == POLICY_LEVEL_OFF) {
+            if (mPolicyLevel == POLICY_LEVEL_OFF) {
                 // Current policy is OFF, so there's no change to notify listeners of.
                 return;
             }
@@ -428,14 +424,14 @@
         boolean changed = false;
         Policy newFullPolicy = Policy.fromSettings(setting, deviceSpecificSetting,
                 DEFAULT_FULL_POLICY);
-        if (getPolicyLevelLocked() == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) {
+        if (mPolicyLevel == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) {
             changed = true;
         }
         mFullPolicy = newFullPolicy;
 
         mDefaultAdaptivePolicy = Policy.fromSettings(adaptiveSetting, adaptiveDeviceSpecificSetting,
                 DEFAULT_ADAPTIVE_POLICY);
-        if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE
+        if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE
                 && !mAdaptivePolicy.equals(mDefaultAdaptivePolicy)) {
             changed = true;
         }
@@ -451,8 +447,9 @@
     @GuardedBy("mLock")
     private void updatePolicyDependenciesLocked() {
         final Policy rawPolicy = getCurrentRawPolicyLocked();
-
         final int locationMode;
+
+        invalidatePowerSaveModeCaches();
         if (mCarModeEnabled
                 && rawPolicy.locationMode != PowerManager.LOCATION_MODE_NO_CHANGE
                 && rawPolicy.locationMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY) {
@@ -461,7 +458,8 @@
         } else {
             locationMode = rawPolicy.locationMode;
         }
-        mEffectivePolicy = new Policy(
+
+        mEffectivePolicyRaw = new Policy(
                 rawPolicy.adjustBrightnessFactor,
                 rawPolicy.advertiseIsEnabled,
                 rawPolicy.deferFullBackup,
@@ -489,24 +487,24 @@
 
         final StringBuilder sb = new StringBuilder();
 
-        if (mEffectivePolicy.forceAllAppsStandby) sb.append("A");
-        if (mEffectivePolicy.forceBackgroundCheck) sb.append("B");
+        if (mEffectivePolicyRaw.forceAllAppsStandby) sb.append("A");
+        if (mEffectivePolicyRaw.forceBackgroundCheck) sb.append("B");
 
-        if (mEffectivePolicy.disableVibration) sb.append("v");
-        if (mEffectivePolicy.disableAnimation) sb.append("a");
-        if (mEffectivePolicy.disableSoundTrigger) sb.append("s");
-        if (mEffectivePolicy.deferFullBackup) sb.append("F");
-        if (mEffectivePolicy.deferKeyValueBackup) sb.append("K");
-        if (mEffectivePolicy.enableFirewall) sb.append("f");
-        if (mEffectivePolicy.enableDataSaver) sb.append("d");
-        if (mEffectivePolicy.enableAdjustBrightness) sb.append("b");
+        if (mEffectivePolicyRaw.disableVibration) sb.append("v");
+        if (mEffectivePolicyRaw.disableAnimation) sb.append("a");
+        if (mEffectivePolicyRaw.disableSoundTrigger) sb.append("s");
+        if (mEffectivePolicyRaw.deferFullBackup) sb.append("F");
+        if (mEffectivePolicyRaw.deferKeyValueBackup) sb.append("K");
+        if (mEffectivePolicyRaw.enableFirewall) sb.append("f");
+        if (mEffectivePolicyRaw.enableDataSaver) sb.append("d");
+        if (mEffectivePolicyRaw.enableAdjustBrightness) sb.append("b");
 
-        if (mEffectivePolicy.disableLaunchBoost) sb.append("l");
-        if (mEffectivePolicy.disableOptionalSensors) sb.append("S");
-        if (mEffectivePolicy.disableAod) sb.append("o");
-        if (mEffectivePolicy.enableQuickDoze) sb.append("q");
+        if (mEffectivePolicyRaw.disableLaunchBoost) sb.append("l");
+        if (mEffectivePolicyRaw.disableOptionalSensors) sb.append("S");
+        if (mEffectivePolicyRaw.disableAod) sb.append("o");
+        if (mEffectivePolicyRaw.enableQuickDoze) sb.append("q");
 
-        sb.append(mEffectivePolicy.locationMode);
+        sb.append(mEffectivePolicyRaw.locationMode);
 
         mEventLogKeys = sb.toString();
     }
@@ -969,14 +967,14 @@
      */
     boolean setPolicyLevel(@PolicyLevel int level) {
         synchronized (mLock) {
-            if (getPolicyLevelLocked() == level) {
+            if (mPolicyLevel == level) {
                 return false;
             }
             switch (level) {
                 case POLICY_LEVEL_FULL:
                 case POLICY_LEVEL_ADAPTIVE:
                 case POLICY_LEVEL_OFF:
-                    setPolicyLevelLocked(level);
+                    mPolicyLevel = level;
                     break;
                 default:
                     Slog.wtf(TAG, "setPolicyLevel invalid level given: " + level);
@@ -998,7 +996,7 @@
         }
 
         mAdaptivePolicy = p;
-        if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE) {
+        if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE) {
             updatePolicyDependenciesLocked();
             return true;
         }
@@ -1011,11 +1009,11 @@
     }
 
     private Policy getCurrentPolicyLocked() {
-        return mEffectivePolicy;
+        return mEffectivePolicyRaw;
     }
 
     private Policy getCurrentRawPolicyLocked() {
-        switch (getPolicyLevelLocked()) {
+        switch (mPolicyLevel) {
             case POLICY_LEVEL_FULL:
                 return mFullPolicy;
             case POLICY_LEVEL_ADAPTIVE:
@@ -1077,12 +1075,12 @@
 
             pw.println("  mAccessibilityEnabled=" + mAccessibilityEnabled);
             pw.println("  mCarModeEnabled=" + mCarModeEnabled);
-            pw.println("  mPolicyLevel=" + getPolicyLevelLocked());
+            pw.println("  mPolicyLevel=" + mPolicyLevel);
 
             dumpPolicyLocked(pw, "  ", "full", mFullPolicy);
             dumpPolicyLocked(pw, "  ", "default adaptive", mDefaultAdaptivePolicy);
             dumpPolicyLocked(pw, "  ", "current adaptive", mAdaptivePolicy);
-            dumpPolicyLocked(pw, "  ", "effective", mEffectivePolicy);
+            dumpPolicyLocked(pw, "  ", "effective", mEffectivePolicyRaw);
         }
     }
 
@@ -1170,20 +1168,4 @@
             }
         }
     }
-
-    /** Non-blocking getter exists as a reminder not to modify cached fields directly */
-    @GuardedBy("mLock")
-    private int getPolicyLevelLocked() {
-        return mPolicyLevelRaw;
-    }
-
-    @GuardedBy("mLock")
-    private void setPolicyLevelLocked(int level) {
-        if (mPolicyLevelRaw == level) {
-            return;
-        }
-        // Under lock, invalidate before set ensures caches won't return stale values.
-        invalidatePowerSaveModeCaches();
-        mPolicyLevelRaw = level;
-    }
 }
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 801d75b..f6d46e2 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -25,6 +25,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.pm.VersionedPackage;
 import android.content.rollback.PackageRollbackInfo;
 import android.content.rollback.RollbackInfo;
@@ -41,10 +42,13 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.LocalServices;
 import com.android.server.PackageWatchdog;
 import com.android.server.PackageWatchdog.FailureReasons;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
 import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
+import com.android.server.pm.ApexManager;
+import com.android.server.pm.parsing.pkg.AndroidPackage;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -71,6 +75,7 @@
 
     private final Context mContext;
     private final Handler mHandler;
+    private final ApexManager mApexManager;
     private final File mLastStagedRollbackIdsFile;
     // Staged rollback ids that have been committed but their session is not yet ready
     @GuardedBy("mPendingStagedRollbackIds")
@@ -85,6 +90,7 @@
         dataDir.mkdirs();
         mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids");
         PackageWatchdog.getInstance(mContext).registerHealthObserver(this);
+        mApexManager = ApexManager.getInstance();
     }
 
     @Override
@@ -302,6 +308,18 @@
      * Returns true if the package name is the name of a module.
      */
     private boolean isModule(String packageName) {
+        // Check if the package is an APK inside an APEX. If it is, use the parent APEX package when
+        // querying PackageManager.
+        PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+        AndroidPackage apkPackage = pmi.getPackage(packageName);
+        if (apkPackage != null) {
+            String apexPackageName = mApexManager.getActiveApexPackageNameContainingPackage(
+                    apkPackage);
+            if (apexPackageName != null) {
+                packageName = apexPackageName;
+            }
+        }
+
         PackageManager pm = mContext.getPackageManager();
         try {
             return pm.getModuleInfo(packageName, 0) != null;
diff --git a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java
index 8fe6da5..390340a 100644
--- a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java
+++ b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java
@@ -88,6 +88,47 @@
     }
 
     @Override
+    public void openGamepadBridge(IBinder token, String name) throws RemoteException {
+        if (DEBUG) {
+            Slog.d(TAG, String.format("openGamepadBridge(), token: %s, name: %s", token, name));
+        }
+
+        synchronized (mLock) {
+            if (mBridgeMap.containsKey(token)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "InputBridge already exists");
+                }
+            } else {
+                final long idToken = Binder.clearCallingIdentity();
+                try {
+                    mBridgeMap.put(token, UinputBridge.openGamepad(token, name));
+                    token.linkToDeath(new IBinder.DeathRecipient() {
+                        @Override
+                        public void binderDied() {
+                            closeInputBridge(token);
+                        }
+                    }, 0);
+                } catch (IOException e) {
+                    Slog.e(TAG, "Cannot create device for " + name);
+                    return;
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Token is already dead");
+                    closeInputBridge(token);
+                    return;
+                } finally {
+                    Binder.restoreCallingIdentity(idToken);
+                }
+            }
+        }
+
+        try {
+            mProvider.onInputBridgeConnected(token);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed remote call to onInputBridgeConnected");
+        }
+    }
+
+    @Override
     public void closeInputBridge(IBinder token) {
         if (DEBUG) {
             Slog.d(TAG, "closeInputBridge(), token: " + token);
@@ -96,6 +137,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.remove(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -117,6 +159,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -145,6 +188,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -166,6 +210,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -188,6 +233,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -209,6 +255,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -230,6 +277,7 @@
         synchronized (mLock) {
             UinputBridge inputBridge = mBridgeMap.get(token);
             if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
                 return;
             }
 
@@ -241,4 +289,67 @@
             }
         }
     }
+
+    @Override
+    public void sendGamepadKeyUp(IBinder token, int keyIndex) {
+        if (DEBUG_KEYS) {
+            Slog.d(TAG, String.format("sendGamepadKeyUp(), token: %s", token));
+        }
+        synchronized (mLock) {
+            UinputBridge inputBridge = mBridgeMap.get(token);
+            if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
+                return;
+            }
+
+            final long idToken = Binder.clearCallingIdentity();
+            try {
+                inputBridge.sendGamepadKey(token, keyIndex, false);
+            } finally {
+                Binder.restoreCallingIdentity(idToken);
+            }
+        }
+    }
+
+    @Override
+    public void sendGamepadKeyDown(IBinder token, int keyCode) {
+        if (DEBUG_KEYS) {
+            Slog.d(TAG, String.format("sendGamepadKeyDown(), token: %s", token));
+        }
+        synchronized (mLock) {
+            UinputBridge inputBridge = mBridgeMap.get(token);
+            if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
+                return;
+            }
+
+            final long idToken = Binder.clearCallingIdentity();
+            try {
+                inputBridge.sendGamepadKey(token, keyCode, true);
+            } finally {
+                Binder.restoreCallingIdentity(idToken);
+            }
+        }
+    }
+
+    @Override
+    public void sendGamepadAxisValue(IBinder token, int axis, float value) {
+        if (DEBUG_KEYS) {
+            Slog.d(TAG, String.format("sendGamepadAxisValue(), token: %s", token));
+        }
+        synchronized (mLock) {
+            UinputBridge inputBridge = mBridgeMap.get(token);
+            if (inputBridge == null) {
+                Slog.w(TAG, String.format("Input bridge not found for token: %s", token));
+                return;
+            }
+
+            final long idToken = Binder.clearCallingIdentity();
+            try {
+                inputBridge.sendGamepadAxisValue(token, axis, value);
+            } finally {
+                Binder.restoreCallingIdentity(idToken);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/tv/UinputBridge.java b/services/core/java/com/android/server/tv/UinputBridge.java
index a2fe5fc..1dc201d 100644
--- a/services/core/java/com/android/server/tv/UinputBridge.java
+++ b/services/core/java/com/android/server/tv/UinputBridge.java
@@ -42,21 +42,27 @@
     /** Opens a gamepad - will support gamepad key and axis sending */
     private static native long nativeGamepadOpen(String name, String uniqueId);
 
-    /** Marks the specified key up/down for a gamepad */
-    private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down);
+    /**
+     * Marks the specified key up/down for a gamepad.
+     *
+     *  @param keyCode - a code like BUTTON_MODE, BUTTON_A, BUTTON_B, ...
+     */
+    private static native void nativeSendGamepadKey(long ptr, int keyCode, boolean down);
 
     /**
-     * Gamepads pre-define the following axes:
-     *   - Left joystick X, axis == ABS_X == 0, range [0, 254]
-     *   - Left joystick Y, axis == ABS_Y == 1, range [0, 254]
-     *   - Right joystick X, axis == ABS_RX == 3, range [0, 254]
-     *   - Right joystick Y, axis == ABS_RY == 4, range [0, 254]
-     *   - Left trigger, axis == ABS_Z == 2, range [0, 254]
-     *   - Right trigger, axis == ABS_RZ == 5, range [0, 254]
-     *   - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1]
-     *   - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1]
+     * Send an axis value.
+     *
+     * Available axes are:
+     *  <li> Left joystick: AXIS_X, AXIS_Y
+     *  <li> Right joystick: AXIS_Z, AXIS_RZ
+     *  <li> Analog triggers: AXIS_LTRIGGER, AXIS_RTRIGGER
+     *  <li> DPad: AXIS_HAT_X, AXIS_HAT_Y
+     *
+     * @param axis is a MotionEvent.AXIS_* value.
+     * @param value is a value between -1 and 1 (inclusive)
+     *
      */
-    private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value);
+    private static native void nativeSendGamepadAxisValue(long ptr, int axis, float value);
 
     public UinputBridge(IBinder token, String name, int width, int height, int maxPointers)
                         throws IOException {
@@ -163,26 +169,19 @@
      *  @param keyIndex - the index of the w3-spec key
      *  @param down - is the key pressed ?
      */
-    public void sendGamepadKey(IBinder token, int keyIndex, boolean down) {
+    public void sendGamepadKey(IBinder token, int keyCode, boolean down) {
         if (isTokenValid(token)) {
-            nativeSendGamepadKey(mPtr, keyIndex, down);
+            nativeSendGamepadKey(mPtr, keyCode, down);
         }
     }
 
-    /** Send a gamepad axis value.
-     *   - Left joystick X, axis == ABS_X == 0, range [0, 254]
-     *   - Left joystick Y, axis == ABS_Y == 1, range [0, 254]
-     *   - Right joystick X, axis == ABS_RX == 3, range [0, 254]
-     *   - Right joystick Y, axis == ABS_RY == 4, range [0, 254]
-     *   - Left trigger, axis == ABS_Z == 2, range [0, 254]
-     *   - Right trigger, axis == ABS_RZ == 5, range [0, 254]
-     *   - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1]
-     *   - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1]
+    /**
+     * Send a gamepad axis value.
      *
-     * @param axis is the axis index
-     * @param value is the value to set for that axis
+     * @param axis is the axis code (MotionEvent.AXIS_*)
+     * @param value is the value to set for that axis in [-1, 1]
      */
-    public void sendGamepadAxisValue(IBinder token, int axis, int value) {
+    public void sendGamepadAxisValue(IBinder token, int axis, float value) {
         if (isTokenValid(token)) {
             nativeSendGamepadAxisValue(mPtr, axis, value);
         }
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 3f9f95c..abccf99 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -1245,6 +1245,16 @@
                     }
                 }
 
+                for (int i = dc.mShellRoots.size() - 1; i >= 0; --i) {
+                    final WindowInfo info = dc.mShellRoots.valueAt(i).getWindowInfo();
+                    if (info == null) {
+                        continue;
+                    }
+                    info.layer = addedWindows.size();
+                    windows.add(info);
+                    addedWindows.add(info.token);
+                }
+
                 // Remove child/parent references to windows that were not added.
                 final int windowCount = windows.size();
                 for (int i = 0; i < windowCount; i++) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e76eda0..2648c86 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4712,7 +4712,7 @@
      */
     private boolean shouldBeResumed(ActivityRecord activeActivity) {
         return shouldMakeActive(activeActivity) && isFocusable()
-                && getRootTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE
+                && getTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE
                 && canResumeByCompat();
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 0f57496..ff43e77 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -18,11 +18,8 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
@@ -142,13 +139,11 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.service.voice.IVoiceInteractionSession;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
 import android.view.DisplayInfo;
-import android.view.SurfaceControl;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -157,7 +152,6 @@
 import com.android.internal.util.function.pooled.PooledConsumer;
 import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
-import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.Watchdog;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.ActivityManagerService.ItemMatcher;
@@ -276,11 +270,6 @@
 
     Rect mPreAnimationBounds = new Rect();
 
-    /**
-     * For {@link #prepareSurfaces}.
-     */
-    private final Point mLastSurfaceSize = new Point();
-
     private final AnimatingActivityRegistry mAnimatingActivityRegistry =
             new AnimatingActivityRegistry();
 
@@ -606,10 +595,6 @@
 
         super.onConfigurationChanged(newParentConfig);
 
-        // Only need to update surface size here since the super method will handle updating
-        // surface position.
-        updateSurfaceSize(getPendingTransaction());
-
         final TaskDisplayArea taskDisplayArea = getDisplayArea();
         if (taskDisplayArea == null) {
             return;
@@ -3262,61 +3247,14 @@
         scheduleAnimation();
     }
 
-    /**
-     * Calculate an amount by which to expand the stack bounds in each direction.
-     * Used to make room for shadows in the pinned windowing mode.
-     */
-    int getStackOutset() {
-        // If we are drawing shadows on the task then don't outset the stack.
-        if (mWmService.mRenderShadowsInCompositor) {
-            return 0;
-        }
-        DisplayContent displayContent = getDisplayContent();
-        if (inPinnedWindowingMode() && displayContent != null) {
-            final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics();
-
-            // We multiply by two to match the client logic for converting view elevation
-            // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets}
-            return (int) Math.ceil(
-                    mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics)
-                            * 2);
-        }
-        return 0;
-    }
-
     @Override
     void getRelativePosition(Point outPos) {
         super.getRelativePosition(outPos);
-        final int outset = getStackOutset();
+        final int outset = getTaskOutset();
         outPos.x -= outset;
         outPos.y -= outset;
     }
 
-    private void updateSurfaceSize(SurfaceControl.Transaction transaction) {
-        if (mSurfaceControl == null) {
-            return;
-        }
-
-        final Rect stackBounds = getBounds();
-        int width = stackBounds.width();
-        int height = stackBounds.height();
-
-        final int outset = getStackOutset();
-        width += 2 * outset;
-        height += 2 * outset;
-
-        if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
-            return;
-        }
-        transaction.setWindowCrop(mSurfaceControl, width, height);
-        mLastSurfaceSize.set(width, height);
-    }
-
-    @VisibleForTesting
-    Point getLastSurfaceSize() {
-        return mLastSurfaceSize;
-    }
-
     @Override
     void onDisplayChanged(DisplayContent dc) {
         super.onDisplayChanged(dc);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a47cdc6..e261632 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -26,7 +26,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
@@ -574,7 +573,7 @@
     /** Corner radius that windows should have in order to match the display. */
     private final float mWindowCornerRadius;
 
-    private final SparseArray<ShellRoot> mShellRoots = new SparseArray<>();
+    final SparseArray<ShellRoot> mShellRoots = new SparseArray<>();
     RemoteInsetsControlTarget mRemoteInsetsControlTarget = null;
     private final IBinder.DeathRecipient mRemoteInsetsDeath =
             () -> {
@@ -5450,6 +5449,46 @@
         return mWmService.mDisplayManagerInternal.getDisplayPosition(getDisplayId());
     }
 
+    /**
+     * Locates the appropriate target window for scroll capture. The search progresses top to
+     * bottom.
+     * If {@code searchBehind} is non-null, the search will only consider windows behind this one.
+     * If a valid taskId is specified, the target window must belong to the given task.
+     *
+     * @param searchBehind a window used to filter the search to windows behind it, or null to begin
+     *                     the search at the top window of the display
+     * @param taskId       specifies the id of a task the result must belong to or
+     *                     {@link android.app.ActivityTaskManager#INVALID_TASK_ID INVALID_TASK_ID}
+     *                     to match any window
+     * @return the located window or null if none could be found matching criteria
+     */
+    @Nullable
+    WindowState findScrollCaptureTargetWindow(@Nullable WindowState searchBehind, int taskId) {
+        return getWindow(new Predicate<WindowState>() {
+            boolean behindTopWindow = (searchBehind == null); // optional filter
+            @Override
+            public boolean test(WindowState nextWindow) {
+                // Skip through all windows until we pass topWindow (if specified)
+                if (!behindTopWindow) {
+                    if (nextWindow == searchBehind) {
+                        behindTopWindow = true;
+                    }
+                    return false; /* continue */
+                }
+                if (taskId != INVALID_TASK_ID) {
+                    Task task = nextWindow.getTask();
+                    if (task == null || !task.isTaskId(taskId)) {
+                        return false; /* continue */
+                    }
+                }
+                if (!nextWindow.canReceiveKeys()) {
+                    return false; /* continue */
+                }
+                return true; /* stop */
+            }
+        });
+    }
+
     class RemoteInsetsControlTarget implements InsetsControlTarget {
         private final IDisplayWindowInsetsController mRemoteInsetsController;
 
diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java
index 20738ed..803bec8 100644
--- a/services/core/java/com/android/server/wm/DockedStackDividerController.java
+++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java
@@ -45,6 +45,11 @@
 
     void setTouchRegion(Rect touchRegion) {
         mTouchRegion.set(touchRegion);
+        // We need to report touchable region changes to accessibility.
+        if (mDisplayContent.mWmService.mAccessibilityController != null) {
+            mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMovedLocked(
+                    mDisplayContent.getDisplayId());
+        }
     }
 
     void getTouchRegion(Rect outRegion) {
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 8b34b9b..656dca5 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -487,7 +487,8 @@
                     || w.cantReceiveTouchInput()) {
                 if (w.mWinAnimator.hasSurface()) {
                     mInputTransaction.setInputWindowInfo(
-                            w.mWinAnimator.mSurfaceController.mSurfaceControl, mInvalidInputWindow);
+                        w.mWinAnimator.mSurfaceController.getClientViewRootSurface(),
+                        mInvalidInputWindow);
                 }
                 // Skip this window because it cannot possibly receive input.
                 return;
@@ -560,7 +561,8 @@
 
             if (w.mWinAnimator.hasSurface()) {
                 mInputTransaction.setInputWindowInfo(
-                        w.mWinAnimator.mSurfaceController.mSurfaceControl, inputWindowHandle);
+                    w.mWinAnimator.mSurfaceController.getClientViewRootSurface(),
+                    inputWindowHandle);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5f33ea1..9d44cad 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -597,8 +597,8 @@
             return startAnimation(initializeBuilder()
                             .setSurfaceControl(mScreenshotLayer)
                             .setAnimationLeashParent(mDisplayContent.getOverlayLayer())
-                            .setWidth(mWidth)
-                            .setHeight(mHeight)
+                            .setWidth(mDisplayContent.getSurfaceWidth())
+                            .setHeight(mDisplayContent.getSurfaceHeight())
                             .build(),
                     createWindowAnimationSpec(mRotateAlphaAnimation),
                     this::onAnimationEnd);
diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java
index 701feff..0b1760d 100644
--- a/services/core/java/com/android/server/wm/ShellRoot.java
+++ b/services/core/java/com/android/server/wm/ShellRoot.java
@@ -23,12 +23,14 @@
 
 import android.annotation.NonNull;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 import android.view.DisplayInfo;
 import android.view.IWindow;
 import android.view.SurfaceControl;
+import android.view.WindowInfo;
 import android.view.animation.Animation;
 
 /**
@@ -102,5 +104,27 @@
         mToken.startAnimation(mToken.getPendingTransaction(), adapter, false /* hidden */,
                 ANIMATION_TYPE_WINDOW_ANIMATION, null /* animationFinishedCallback */);
     }
+
+    WindowInfo getWindowInfo() {
+        if (mToken.windowType != TYPE_DOCK_DIVIDER) {
+            return null;
+        }
+        if (!mDisplayContent.getDefaultTaskDisplayArea().isSplitScreenModeActivated()) {
+            return null;
+        }
+        WindowInfo windowInfo = WindowInfo.obtain();
+        windowInfo.displayId = mToken.getDisplayArea().getDisplayContent().mDisplayId;
+        windowInfo.type = mToken.windowType;
+        windowInfo.layer = mToken.getWindowLayerFromType();
+        windowInfo.token = mClient.asBinder();
+        windowInfo.title = "Splitscreen Divider";
+        windowInfo.focused = false;
+        windowInfo.inPictureInPicture = false;
+        windowInfo.hasFlagWatchOutsideTouch = false;
+        final Rect regionRect = new Rect();
+        mDisplayContent.getDockedDividerController().getTouchRegion(regionRect);
+        windowInfo.regionInScreen.set(regionRect);
+        return windowInfo;
+    }
 }
 
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 66ca0ac..df5cfee 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -120,6 +120,7 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Debug;
 import android.os.IBinder;
@@ -213,7 +214,6 @@
 
     static final int INVALID_MIN_SIZE = -1;
     private float mShadowRadius = 0;
-    private final Rect mLastSurfaceCrop = new Rect();
 
     /**
      * The modes to control how the stack is moved to the front when calling {@link Task#reparent}.
@@ -397,6 +397,7 @@
 
     private Dimmer mDimmer = new Dimmer(this);
     private final Rect mTmpDimBoundsRect = new Rect();
+    private final Point mLastSurfaceSize = new Point();
 
     /** @see #setCanAffectSystemUiFlags */
     private boolean mCanAffectSystemUiFlags = true;
@@ -1943,6 +1944,10 @@
         mTmpPrevBounds.set(getBounds());
         final boolean wasInMultiWindowMode = inMultiWindowMode();
         super.onConfigurationChanged(newParentConfig);
+        // Only need to update surface size here since the super method will handle updating
+        // surface position.
+        updateSurfaceSize(getPendingTransaction());
+
         if (wasInMultiWindowMode != inMultiWindowMode()) {
             mStackSupervisor.scheduleUpdateMultiWindowMode(this);
         }
@@ -1995,6 +2000,57 @@
         return (prevWinMode == WINDOWING_MODE_FREEFORM) != (newWinMode == WINDOWING_MODE_FREEFORM);
     }
 
+    void updateSurfaceSize(SurfaceControl.Transaction transaction) {
+        if (mSurfaceControl == null || mCreatedByOrganizer) {
+            return;
+        }
+
+        // Apply crop to root tasks only and clear the crops of the descendant tasks.
+        int width = 0;
+        int height = 0;
+        if (isRootTask()) {
+            final Rect taskBounds = getBounds();
+            width = taskBounds.width();
+            height = taskBounds.height();
+
+            final int outset = getTaskOutset();
+            width += 2 * outset;
+            height += 2 * outset;
+        }
+        if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
+            return;
+        }
+        transaction.setWindowCrop(mSurfaceControl, width, height);
+        mLastSurfaceSize.set(width, height);
+    }
+
+    /**
+     * Calculate an amount by which to expand the task bounds in each direction.
+     * Used to make room for shadows in the pinned windowing mode.
+     */
+    int getTaskOutset() {
+        // If we are drawing shadows on the task then don't outset the stack.
+        if (mWmService.mRenderShadowsInCompositor) {
+            return 0;
+        }
+        DisplayContent displayContent = getDisplayContent();
+        if (inPinnedWindowingMode() && displayContent != null) {
+            final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics();
+
+            // We multiply by two to match the client logic for converting view elevation
+            // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets}
+            return (int) Math.ceil(
+                    mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics)
+                            * 2);
+        }
+        return 0;
+    }
+
+    @VisibleForTesting
+    Point getLastSurfaceSize() {
+        return mLastSurfaceSize;
+    }
+
     @VisibleForTesting
     boolean isInChangeTransition() {
         return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransit(mTransit);
@@ -2225,14 +2281,16 @@
         }
         density *= DisplayMetrics.DENSITY_DEFAULT_SCALE;
 
+        // If bounds have been overridden at this level, restrict config resources to these bounds
+        // rather than the parent because the overridden bounds can be larger than the parent.
+        boolean hasOverrideBounds = false;
+
         final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds();
-        if (resolvedBounds == null) {
-            mTmpFullBounds.setEmpty();
+        if (resolvedBounds == null || resolvedBounds.isEmpty()) {
+            mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds());
         } else {
             mTmpFullBounds.set(resolvedBounds);
-        }
-        if (mTmpFullBounds.isEmpty()) {
-            mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds());
+            hasOverrideBounds = true;
         }
 
         Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
@@ -2244,7 +2302,16 @@
         // the out bounds doesn't need to be restricted by the parent.
         final boolean insideParentBounds = compatInsets == null;
         if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) {
-            final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
+            Rect parentAppBounds;
+            if (hasOverrideBounds) {
+                // Since we overrode the bounds, restrict appBounds to display non-decor rather
+                // than parent. Otherwise, it won't match the overridden bounds.
+                final TaskDisplayArea displayArea = getDisplayArea();
+                parentAppBounds = displayArea != null
+                        ? displayArea.getConfiguration().windowConfiguration.getAppBounds() : null;
+            } else {
+                parentAppBounds = parentConfig.windowConfiguration.getAppBounds();
+            }
             if (parentAppBounds != null && !parentAppBounds.isEmpty()) {
                 outAppBounds.intersect(parentAppBounds);
             }
@@ -2291,13 +2358,13 @@
 
             if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) {
                 final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density);
-                inOutConfig.screenWidthDp = insideParentBounds
+                inOutConfig.screenWidthDp = (insideParentBounds && !hasOverrideBounds)
                         ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp)
                         : overrideScreenWidthDp;
             }
             if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) {
                 final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density);
-                inOutConfig.screenHeightDp = insideParentBounds
+                inOutConfig.screenHeightDp = (insideParentBounds && !hasOverrideBounds)
                         ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp)
                         : overrideScreenHeightDp;
             }
@@ -2344,27 +2411,27 @@
         mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds());
         super.resolveOverrideConfiguration(newParentConfig);
 
-        // Resolve override windowing mode to fullscreen for home task (even on freeform
-        // display), or split-screen-secondary if in split-screen mode.
         int windowingMode =
                 getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
+
+        // Resolve override windowing mode to fullscreen for home task (even on freeform
+        // display), or split-screen if in split-screen mode.
         if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) {
             final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode();
-            windowingMode = parentWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                    ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
-                    : WINDOWING_MODE_FULLSCREEN;
+            windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode)
+                    ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN;
             getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode);
         }
 
-        if (!isLeafTask()) {
-            // Compute configuration overrides for tasks that created by organizer, so that
-            // organizer can get the correct configuration from those tasks.
-            if (mCreatedByOrganizer) {
-                computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
-            }
-            return;
+        if (isLeafTask()) {
+            resolveLeafOnlyOverrideConfigs(newParentConfig);
         }
+        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
+    }
 
+    void resolveLeafOnlyOverrideConfigs(Configuration newParentConfig) {
+        int windowingMode =
+                getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode();
         if (windowingMode == WINDOWING_MODE_UNDEFINED) {
             windowingMode = newParentConfig.windowConfiguration.getWindowingMode();
         }
@@ -2404,7 +2471,6 @@
                 outOverrideBounds.offset(0, offsetTop);
             }
         }
-        computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig);
     }
 
     /**
@@ -2803,28 +2869,6 @@
         return boundsChange;
     }
 
-    private void updateSurfaceCrop() {
-        // Only update the crop if we are drawing shadows on the task.
-        if (mSurfaceControl == null || !mWmService.mRenderShadowsInCompositor || !isRootTask()) {
-            return;
-        }
-
-        if (inSplitScreenWindowingMode()) {
-            // inherit crop from parent
-            mTmpRect.setEmpty();
-        } else {
-            getBounds(mTmpRect);
-        }
-
-        mTmpRect.offsetTo(0, 0);
-        if (mLastSurfaceCrop.equals(mTmpRect)) {
-            return;
-        }
-
-        getPendingTransaction().setWindowCrop(mSurfaceControl, mTmpRect);
-        mLastSurfaceCrop.set(mTmpRect);
-    }
-
     @Override
     public boolean onDescendantOrientationChanged(IBinder freezeDisplayToken,
             ConfigurationContainer requestingContainer) {
@@ -3453,7 +3497,6 @@
             mTmpDimBoundsRect.offsetTo(0, 0);
         }
 
-        updateSurfaceCrop();
         updateShadowsRadius(isFocused(), getPendingTransaction());
 
         if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index cb9b332..25791c7 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -267,16 +267,14 @@
 
     @Override
     void addChild(ActivityStack stack, int position) {
+        if (DEBUG_STACK) Slog.d(TAG_WM, "Set stack=" + stack + " on taskDisplayArea=" + this);
         addStackReferenceIfNeeded(stack);
         position = findPositionForStack(position, stack, true /* adding */);
 
         super.addChild(stack, position);
         mAtmService.updateSleepIfNeededLocked();
 
-        // The reparenting case is handled in WindowContainer.
-        if (!stack.mReparenting) {
-            mDisplayContent.setLayoutNeeded();
-        }
+        positionStackAt(stack, position);
     }
 
     @Override
@@ -638,12 +636,6 @@
         }
     }
 
-    void addStack(ActivityStack stack, int position) {
-        if (DEBUG_STACK) Slog.d(TAG_WM, "Set stack=" + stack + " on taskDisplayArea=" + this);
-        addChild(stack, position);
-        positionStackAt(stack, position);
-    }
-
     void onStackRemoved(ActivityStack stack) {
         if (ActivityTaskManagerDebugConfig.DEBUG_STACK) {
             Slog.v(TAG_STACK, "removeStack: detaching " + stack + " from displayId="
@@ -787,7 +779,7 @@
                 }
             } else if (stack.getDisplayArea() != this || !stack.isRootTask()) {
                 if (stack.getParent() == null) {
-                    addStack(stack, position);
+                    addChild(stack, position);
                 } else {
                     stack.reparent(this, onTop);
                 }
@@ -943,7 +935,7 @@
                 positionStackAtTop((ActivityStack) launchRootTask, false /* includingParents */);
             }
         } else {
-            addStack(stack, onTop ? POSITION_TOP : POSITION_BOTTOM);
+            addChild(stack, onTop ? POSITION_TOP : POSITION_BOTTOM);
             stack.setWindowingMode(windowingMode, true /* creating */);
         }
         return stack;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 899ab24..f3e2992 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -662,9 +662,11 @@
                 //       position that takes into account the removed child (if the index of the
                 //       child < position, then the position should be adjusted). We should consider
                 //       doing this adjustment here and remove any adjustments in the callers.
-                mChildren.remove(child);
-                mChildren.add(position, child);
-                onChildPositionChanged(child);
+                if (mChildren.indexOf(child) != position) {
+                    mChildren.remove(child);
+                    mChildren.add(position, child);
+                    onChildPositionChanged(child);
+                }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f55a1b3..a501414 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -219,6 +219,7 @@
 import android.view.IPinnedStackListener;
 import android.view.IRecentsAnimationRunner;
 import android.view.IRotationWatcher;
+import android.view.IScrollCaptureController;
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWallpaperVisibilityListener;
 import android.view.IWindow;
@@ -6837,6 +6838,58 @@
         }
     }
 
+    /**
+     * Forwards a scroll capture request to the appropriate window, if available.
+     *
+     * @param displayId the display for the request
+     * @param behindClient token for a window, used to filter the search to windows behind it
+     * @param taskId specifies the id of a task the result must belong to or -1 to ignore task ids
+     * @param controller the controller to receive results; a call to either
+     *      {@link IScrollCaptureController#onClientConnected} or
+     *      {@link IScrollCaptureController#onClientUnavailable}.
+     */
+    public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId,
+            IScrollCaptureController controller) {
+        if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) {
+            throw new SecurityException("Requires READ_FRAME_BUFFER permission");
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                DisplayContent dc = mRoot.getDisplayContent(displayId);
+                if (dc == null) {
+                    ProtoLog.e(WM_ERROR,
+                            "Invalid displayId for requestScrollCapture: %d", displayId);
+                    controller.onClientUnavailable();
+                    return;
+                }
+                WindowState topWindow = null;
+                if (behindClient != null) {
+                    topWindow = windowForClientLocked(null, behindClient, /* throwOnError*/ true);
+                }
+                WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId);
+                if (targetWindow == null) {
+                    controller.onClientUnavailable();
+                    return;
+                }
+                // Forward to the window for handling.
+                try {
+                    targetWindow.mClient.requestScrollCapture(controller);
+                } catch (RemoteException e) {
+                    ProtoLog.w(WM_ERROR,
+                            "requestScrollCapture: caught exception dispatching to window."
+                                    + "token=%s", targetWindow.mClient.asBinder());
+                    controller.onClientUnavailable();
+                }
+            }
+        } catch (RemoteException e) {
+            ProtoLog.w(WM_ERROR,
+                    "requestScrollCapture: caught exception dispatching callback: %s", e);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     @Override
     public void dontOverrideDisplayInfo(int displayId) {
         final long token = Binder.clearCallingIdentity();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 83e7ad5..ef690e1 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5289,7 +5289,7 @@
         // to account for it. If we actually have shadows we will
         // then un-inset ourselves by the surfaceInsets.
         if (stack != null) {
-            final int outset = stack.getStackOutset();
+            final int outset = stack.getTaskOutset();
             outPoint.offset(outset, outset);
         }
 
diff --git a/services/core/jni/com_android_server_tv_GamepadKeys.h b/services/core/jni/com_android_server_tv_GamepadKeys.h
index 11fc903..127010f 100644
--- a/services/core/jni/com_android_server_tv_GamepadKeys.h
+++ b/services/core/jni/com_android_server_tv_GamepadKeys.h
@@ -1,77 +1,104 @@
 #ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
 #define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_
 
+#include <android/input.h>
+#include <android/keycodes.h>
 #include <linux/input.h>
 
 namespace android {
 
-// Follows the W3 spec for gamepad buttons and their corresponding mapping into
-// Linux keycodes. Note that gamepads are generally not very well standardized
-// and various controllers will result in different buttons. This mapping tries
-// to be reasonable.
+// The constant array below defines a mapping between "Android" IDs (key code
+// within events) and what is being sent through /dev/uinput.
 //
-// W3 Button spec: https://www.w3.org/TR/gamepad/#remapping
+// The translation back from uinput key codes into android key codes is done through
+// the corresponding key layout files. This file and
 //
-// Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia
-// has "Assistant" and "Share", PS4 has the touchpad button).
+//    data/keyboards/Vendor_18d1_Product_0200.kl
 //
-// To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested.
-static const int GAMEPAD_KEY_CODES[19] = {
-        // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
-        BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant
-        BTN_B, // "East", BTN_B, BTN_EAST have the same constant
-        BTN_X, // "West", Note that this maps to X and NORTH in constants
-        BTN_Y, // "North", Note that this maps to Y and WEST in constants
+// MUST be kept in sync.
+//
+// see https://source.android.com/devices/input/key-layout-files for documentation.
 
-        BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead
-        BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead
-
-        // For triggers, gamepads vary:
-        //   - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends
-        //     TriggerHappy3/4 as digital presses
-        //   - PS4 and Xbox send analog values as ABS_Z/ABS_RZ
-        //   - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently)
-        // As placeholders we chose the stadia trigger-happy values since TL/TR are
-        // sent for bumper button presses
-        BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2"
-        BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2"
-
-        BTN_SELECT, // "Select/Back". Often "options" or similar
-        BTN_START,  // "Start/forward". Often "hamburger" icon
-
-        BTN_THUMBL, // "Left Joystick Pressed"
-        BTN_THUMBR, // "Right Joystick Pressed"
-
-        // For DPads, gamepads generally only send axis changes
-        // on ABS_HAT0X and ABS_HAT0Y.
-        KEY_UP,    // "Digital Pad up"
-        KEY_DOWN,  // "Digital Pad down"
-        KEY_LEFT,  // "Digital Pad left"
-        KEY_RIGHT, // "Digital Pad right"
-
-        BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home)
-
-        BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia
-        BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia
+// Defines axis mapping information between android and
+// uinput axis.
+struct GamepadKey {
+    int32_t androidKeyCode;
+    int linuxUinputKeyCode;
 };
 
-// Defines information for an axis.
-struct Axis {
-    int number;
-    int rangeMin;
-    int rangeMax;
+static const GamepadKey GAMEPAD_KEYS[] = {
+        // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar
+        {AKEYCODE_BUTTON_A, BTN_A},
+        {AKEYCODE_BUTTON_B, BTN_B},
+        {AKEYCODE_BUTTON_X, BTN_X},
+        {AKEYCODE_BUTTON_Y, BTN_Y},
+
+        // Bumper buttons and digital triggers. Triggers generally have
+        // both analog versions (GAS and BRAKE output) and digital ones
+        {AKEYCODE_BUTTON_L1, BTN_TL2},
+        {AKEYCODE_BUTTON_L2, BTN_TL},
+        {AKEYCODE_BUTTON_R1, BTN_TR2},
+        {AKEYCODE_BUTTON_R2, BTN_TR},
+
+        // general actions for controllers
+        {AKEYCODE_BUTTON_SELECT, BTN_SELECT}, // Options or "..."
+        {AKEYCODE_BUTTON_START, BTN_START},   // Menu/Hamburger menu
+        {AKEYCODE_BUTTON_MODE, BTN_MODE},     // "main" button
+
+        // Pressing on the joyticks themselves
+        {AKEYCODE_BUTTON_THUMBL, BTN_THUMBL},
+        {AKEYCODE_BUTTON_THUMBR, BTN_THUMBR},
+
+        // DPAD digital keys. HAT axis events are generally also sent.
+        {AKEYCODE_DPAD_UP, KEY_UP},
+        {AKEYCODE_DPAD_DOWN, KEY_DOWN},
+        {AKEYCODE_DPAD_LEFT, KEY_LEFT},
+        {AKEYCODE_DPAD_RIGHT, KEY_RIGHT},
+
+        // "Extra" controller buttons: some devices have "share" and "assistant"
+        {AKEYCODE_BUTTON_1, BTN_TRIGGER_HAPPY1},
+        {AKEYCODE_BUTTON_2, BTN_TRIGGER_HAPPY2},
+        {AKEYCODE_BUTTON_3, BTN_TRIGGER_HAPPY3},
+        {AKEYCODE_BUTTON_4, BTN_TRIGGER_HAPPY4},
+        {AKEYCODE_BUTTON_5, BTN_TRIGGER_HAPPY5},
+        {AKEYCODE_BUTTON_6, BTN_TRIGGER_HAPPY6},
+        {AKEYCODE_BUTTON_7, BTN_TRIGGER_HAPPY7},
+        {AKEYCODE_BUTTON_8, BTN_TRIGGER_HAPPY8},
+        {AKEYCODE_BUTTON_9, BTN_TRIGGER_HAPPY9},
+        {AKEYCODE_BUTTON_10, BTN_TRIGGER_HAPPY10},
+        {AKEYCODE_BUTTON_11, BTN_TRIGGER_HAPPY11},
+        {AKEYCODE_BUTTON_12, BTN_TRIGGER_HAPPY12},
+        {AKEYCODE_BUTTON_13, BTN_TRIGGER_HAPPY13},
+        {AKEYCODE_BUTTON_14, BTN_TRIGGER_HAPPY14},
+        {AKEYCODE_BUTTON_15, BTN_TRIGGER_HAPPY15},
+        {AKEYCODE_BUTTON_16, BTN_TRIGGER_HAPPY16},
+
+        // Assignment to support global assistant for devices that support it.
+        {AKEYCODE_ASSIST, KEY_ASSISTANT},
+        {AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND},
+};
+
+// Defines axis mapping information between android and
+// uinput axis.
+struct GamepadAxis {
+    int32_t androidAxis;
+    float androidRangeMin;
+    float androidRangeMax;
+    int linuxUinputAxis;
+    int linuxUinputRangeMin;
+    int linuxUinputRangeMax;
 };
 
 // List of all axes supported by a gamepad
-static const Axis GAMEPAD_AXES[] = {
-        {ABS_X, 0, 254},    // Left joystick X
-        {ABS_Y, 0, 254},    // Left joystick Y
-        {ABS_RX, 0, 254},   // Right joystick X
-        {ABS_RY, 0, 254},   // Right joystick Y
-        {ABS_Z, 0, 254},    // Left trigger
-        {ABS_RZ, 0, 254},   // Right trigger
-        {ABS_HAT0X, -1, 1}, // DPad X
-        {ABS_HAT0Y, -1, 1}, // DPad Y
+static const GamepadAxis GAMEPAD_AXES[] = {
+        {AMOTION_EVENT_AXIS_X, -1, 1, ABS_X, 0, 254},           // Left joystick X
+        {AMOTION_EVENT_AXIS_Y, -1, 1, ABS_Y, 0, 254},           // Left joystick Y
+        {AMOTION_EVENT_AXIS_Z, -1, 1, ABS_Z, 0, 254},           // Right joystick X
+        {AMOTION_EVENT_AXIS_RZ, -1, 1, ABS_RZ, 0, 254},         // Right joystick Y
+        {AMOTION_EVENT_AXIS_LTRIGGER, 0, 1, ABS_GAS, 0, 254},   // Left trigger
+        {AMOTION_EVENT_AXIS_RTRIGGER, 0, 1, ABS_BRAKE, 0, 254}, // Right trigger
+        {AMOTION_EVENT_AXIS_HAT_X, -1, 1, ABS_HAT0X, -1, 1},    // DPad X
+        {AMOTION_EVENT_AXIS_HAT_Y, -1, 1, ABS_HAT0Y, -1, 1},    // DPad Y
 };
 
 } // namespace android
diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
index 0e96bd7..6e2e2c5 100644
--- a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
+++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp
@@ -31,27 +31,38 @@
 #include <utils/String8.h>
 
 #include <ctype.h>
-#include <linux/input.h>
-#include <unistd.h>
-#include <sys/time.h>
-#include <time.h>
-#include <stdint.h>
-#include <map>
 #include <fcntl.h>
+#include <linux/input.h>
 #include <linux/uinput.h>
 #include <signal.h>
+#include <stdint.h>
 #include <sys/inotify.h>
 #include <sys/stat.h>
+#include <sys/time.h>
 #include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+#include <unordered_map>
 
 #define SLOT_UNKNOWN -1
 
 namespace android {
 
-static std::map<int32_t,int> keysMap;
-static std::map<int32_t,int32_t> slotsMap;
+#define GOOGLE_VENDOR_ID 0x18d1
+
+#define GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID 0x0100
+#define GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID 0x0200
+
+static std::unordered_map<int32_t, int> keysMap;
+static std::unordered_map<int32_t, int32_t> slotsMap;
 static BitSet32 mtSlots;
 
+// Maps android key code to linux key code.
+static std::unordered_map<int32_t, int> gamepadAndroidToLinuxKeyMap;
+
+// Maps an android gamepad axis to the index within the GAMEPAD_AXES array.
+static std::unordered_map<int32_t, int> gamepadAndroidAxisToIndexMap;
+
 static void initKeysMap() {
     if (keysMap.empty()) {
         for (size_t i = 0; i < NELEM(KEYS); i++) {
@@ -60,16 +71,49 @@
     }
 }
 
+static void initGamepadKeyMap() {
+    if (gamepadAndroidToLinuxKeyMap.empty()) {
+        for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
+            gamepadAndroidToLinuxKeyMap[GAMEPAD_KEYS[i].androidKeyCode] =
+                    GAMEPAD_KEYS[i].linuxUinputKeyCode;
+        }
+    }
+
+    if (gamepadAndroidAxisToIndexMap.empty()) {
+        for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
+            gamepadAndroidAxisToIndexMap[GAMEPAD_AXES[i].androidAxis] = i;
+        }
+    }
+}
+
 static int32_t getLinuxKeyCode(int32_t androidKeyCode) {
-    std::map<int,int>::iterator it = keysMap.find(androidKeyCode);
+    std::unordered_map<int, int>::iterator it = keysMap.find(androidKeyCode);
     if (it != keysMap.end()) {
         return it->second;
     }
     return KEY_UNKNOWN;
 }
 
+static int getGamepadkeyCode(int32_t androidKeyCode) {
+    std::unordered_map<int32_t, int>::iterator it =
+            gamepadAndroidToLinuxKeyMap.find(androidKeyCode);
+    if (it != gamepadAndroidToLinuxKeyMap.end()) {
+        return it->second;
+    }
+    return KEY_UNKNOWN;
+}
+
+static const GamepadAxis* getGamepadAxis(int32_t androidAxisCode) {
+    std::unordered_map<int32_t, int>::iterator it =
+            gamepadAndroidAxisToIndexMap.find(androidAxisCode);
+    if (it == gamepadAndroidToLinuxKeyMap.end()) {
+        return nullptr;
+    }
+    return &GAMEPAD_AXES[it->second];
+}
+
 static int findSlot(int32_t pointerId) {
-    std::map<int,int>::iterator it = slotsMap.find(pointerId);
+    std::unordered_map<int, int>::iterator it = slotsMap.find(pointerId);
     if (it != slotsMap.end()) {
         return it->second;
     }
@@ -107,7 +151,7 @@
 
     // Open /dev/uinput and prepare to register
     // the device with the given name and unique Id
-    bool Open(const char* name, const char* uniqueId);
+    bool Open(const char* name, const char* uniqueId, uint16_t product);
 
     // Checks if the current file descriptor is valid
     bool IsValid() const { return mFd != kInvalidFileDescriptor; }
@@ -141,7 +185,7 @@
     return fd;
 }
 
-bool UInputDescriptor::Open(const char* name, const char* uniqueId) {
+bool UInputDescriptor::Open(const char* name, const char* uniqueId, uint16_t product) {
     if (IsValid()) {
         ALOGE("UInput device already open");
         return false;
@@ -161,6 +205,8 @@
     strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
     mUinputDescriptor.id.version = 1;
     mUinputDescriptor.id.bustype = BUS_VIRTUAL;
+    mUinputDescriptor.id.vendor = GOOGLE_VENDOR_ID;
+    mUinputDescriptor.id.product = product;
 
     // All UInput devices we use process keys
     ioctl(mFd, UI_SET_EVBIT, EV_KEY);
@@ -258,7 +304,7 @@
     initKeysMap();
 
     UInputDescriptor descriptor;
-    if (!descriptor.Open(name, uniqueId)) {
+    if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID)) {
         return nullptr;
     }
 
@@ -277,21 +323,24 @@
 NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) {
     ALOGI("Registering uinput device %s: gamepad", name);
 
+    initGamepadKeyMap();
+
     UInputDescriptor descriptor;
-    if (!descriptor.Open(name, uniqueId)) {
+    if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID)) {
         return nullptr;
     }
 
     // set the keys mapped for gamepads
-    for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
-        descriptor.EnableKey(GAMEPAD_KEY_CODES[i]);
+    for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
+        descriptor.EnableKey(GAMEPAD_KEYS[i].linuxUinputKeyCode);
     }
 
     // define the axes that are required
     descriptor.EnableAxesEvents();
     for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
-        const Axis& axis = GAMEPAD_AXES[i];
-        descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax);
+        const GamepadAxis& axis = GAMEPAD_AXES[i];
+        descriptor.EnableAxis(axis.linuxUinputAxis, axis.linuxUinputRangeMin,
+                              axis.linuxUinputRangeMax);
     }
 
     if (!descriptor.Create()) {
@@ -350,7 +399,7 @@
     }
 }
 
-static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex,
+static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode,
                                  jboolean down) {
     NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
 
@@ -359,16 +408,16 @@
         return;
     }
 
-    if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) {
-        ALOGE("Invalid gamepad key index: %d", keyIndex);
+    int linuxKeyCode = getGamepadkeyCode(keyCode);
+    if (linuxKeyCode == KEY_UNKNOWN) {
+        ALOGE("Gamepad: received an unknown keycode of %d.", keyCode);
         return;
     }
-
-    connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0);
+    connection->sendEvent(EV_KEY, linuxKeyCode, down ? 1 : 0);
 }
 
 static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis,
-                                       jint value) {
+                                       jfloat value) {
     NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr);
 
     if (!connection->IsGamepad()) {
@@ -376,7 +425,25 @@
         return;
     }
 
-    connection->sendEvent(EV_ABS, axis, value);
+    const GamepadAxis* axisInfo = getGamepadAxis(axis);
+    if (axisInfo == nullptr) {
+        ALOGE("Invalid axis: %d", axis);
+        return;
+    }
+
+    if (value > axisInfo->androidRangeMax) {
+        value = axisInfo->androidRangeMax;
+    } else if (value < axisInfo->androidRangeMin) {
+        value = axisInfo->androidRangeMin;
+    }
+
+    // Converts the android range into the device range
+    float movementPercent = (value - axisInfo->androidRangeMin) /
+            (axisInfo->androidRangeMax - axisInfo->androidRangeMin);
+    int axisRawValue = axisInfo->linuxUinputRangeMin +
+            movementPercent * (axisInfo->linuxUinputRangeMax - axisInfo->linuxUinputRangeMin);
+
+    connection->sendEvent(EV_ABS, axisInfo->linuxUinputAxis, axisRawValue);
 }
 
 static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr,
@@ -441,18 +508,20 @@
             }
         }
     } else {
-        for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) {
-            connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0);
+        for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) {
+            connection->sendEvent(EV_KEY, GAMEPAD_KEYS[i].linuxUinputKeyCode, 0);
         }
 
         for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) {
-            const Axis& axis = GAMEPAD_AXES[i];
-            if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) {
+            const GamepadAxis& axis = GAMEPAD_AXES[i];
+
+            if ((axis.linuxUinputAxis == ABS_Z) || (axis.linuxUinputAxis == ABS_RZ)) {
                 // Mark triggers unpressed
-                connection->sendEvent(EV_ABS, axis.number, 0);
+                connection->sendEvent(EV_ABS, axis.linuxUinputAxis, axis.linuxUinputRangeMin);
             } else {
                 // Joysticks and dpad rests on center
-                connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2);
+                connection->sendEvent(EV_ABS, axis.linuxUinputAxis,
+                                      (axis.linuxUinputRangeMin + axis.linuxUinputRangeMax) / 2);
             }
         }
     }
@@ -475,7 +544,7 @@
         {"nativeClear", "(J)V", (void*)nativeClear},
         {"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync},
         {"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey},
-        {"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue},
+        {"nativeSendGamepadAxisValue", "(JIF)V", (void*)nativeSendGamepadAxisValue},
 };
 
 int register_android_server_tv_TvUinputBridge(JNIEnv* env) {
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index 79699bb..f423119 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -1610,6 +1610,8 @@
 
     fsmStep();
 
+    mStatusCondition.notify_all();
+
     return binder::Status::ok();
 }
 
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index f8bcff5..6fe259e 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -45,6 +45,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -54,9 +56,11 @@
 import android.app.AppOpsManager;
 import android.app.IApplicationThread;
 import android.app.IUidObserver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -70,6 +74,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.server.LocalServices;
 import com.android.server.am.ProcessList.IsolatedUidRange;
 import com.android.server.am.ProcessList.IsolatedUidRangeAllocator;
 import com.android.server.appop.AppOpsService;
@@ -77,6 +82,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -115,6 +121,18 @@
         UidRecord.CHANGE_ACTIVE
     };
 
+    private static PackageManagerInternal sPackageManagerInternal;
+
+    @BeforeClass
+    public static void setUpOnce() {
+        sPackageManagerInternal = mock(PackageManagerInternal.class);
+        doReturn(new ComponentName("", "")).when(sPackageManagerInternal)
+                .getSystemUiServiceComponent();
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal);
+    }
+
     @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
 
     private Context mContext = getInstrumentation().getTargetContext();
@@ -258,8 +276,11 @@
         uidRec.hasInternetPermission = true;
         mAms.mProcessList.mActiveUids.put(uid, uidRec);
 
-        final ProcessRecord appRec = new ProcessRecord(mAms, new ApplicationInfo(), TAG, uid);
-        appRec.thread = Mockito.mock(IApplicationThread.class);
+        ApplicationInfo info = new ApplicationInfo();
+        info.packageName = "";
+
+        final ProcessRecord appRec = new ProcessRecord(mAms, info, TAG, uid);
+        appRec.thread = mock(IApplicationThread.class);
         mAms.mProcessList.mLruProcesses.add(appRec);
 
         return uidRec;
@@ -497,7 +518,7 @@
         };
         final IUidObserver[] observers = new IUidObserver.Stub[changesToObserve.length];
         for (int i = 0; i < observers.length; ++i) {
-            observers[i] = Mockito.mock(IUidObserver.Stub.class);
+            observers[i] = mock(IUidObserver.Stub.class);
             when(observers[i].asBinder()).thenReturn((IBinder) observers[i]);
             mAms.registerUidObserver(observers[i], changesToObserve[i] /* which */,
                     ActivityManager.PROCESS_STATE_UNKNOWN /* cutpoint */, null /* caller */);
@@ -610,7 +631,7 @@
      */
     @Test
     public void testDispatchUidChanges_procStateCutpoint() throws RemoteException {
-        final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+        final IUidObserver observer = mock(IUidObserver.Stub.class);
 
         when(observer.asBinder()).thenReturn((IBinder) observer);
         mAms.registerUidObserver(observer, ActivityManager.UID_OBSERVER_PROCSTATE /* which */,
@@ -704,7 +725,7 @@
         assertEquals("No observers registered, so validateUids should be empty",
                 0, mAms.mValidateUids.size());
 
-        final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class);
+        final IUidObserver observer = mock(IUidObserver.Stub.class);
         when(observer.asBinder()).thenReturn((IBinder) observer);
         mAms.registerUidObserver(observer, 0, 0, null);
         // Verify that when observers are registered, then validateUids is correctly updated.
diff --git a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
index d12d804..b2d7177 100644
--- a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
+++ b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java
@@ -22,12 +22,15 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 
 import android.app.ActivityManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
 
 import com.android.server.LocalServices;
 import com.android.server.wm.ActivityTaskManagerService;
@@ -45,6 +48,7 @@
 public class OomAdjusterTests {
     private static Context sContext;
     private static ActivityManagerService sService;
+    private static PackageManagerInternal sPackageManagerInternal;
 
     private ProcessRecord mProcessRecord;
 
@@ -56,6 +60,13 @@
     public static void setUpOnce() {
         sContext = getInstrumentation().getTargetContext();
 
+        sPackageManagerInternal = mock(PackageManagerInternal.class);
+        doReturn(new ComponentName("", "")).when(sPackageManagerInternal)
+                .getSystemUiServiceComponent();
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal);
+
         // We need to run with dexmaker share class loader to make use of
         // ActivityTaskManagerService from wm package.
         runWithDexmakerShareClassLoader(() -> {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java
new file mode 100644
index 0000000..c9dbdd2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings;
+
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
+import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS;
+import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS;
+import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG;
+import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG;
+import static com.android.server.locksettings.LockSettingsStrongAuth.STRONG_AUTH_TIMEOUT_ALARM_TAG;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricIdleTimeoutAlarmListener;
+import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricTimeoutAlarmListener;
+import com.android.server.locksettings.LockSettingsStrongAuth.StrongAuthTimeoutAlarmListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class LockSettingsStrongAuthTest {
+
+    private static final String TAG = LockSettingsStrongAuthTest.class.getSimpleName();
+
+    private static final int PRIMARY_USER_ID = 0;
+
+    private LockSettingsStrongAuth mStrongAuth;
+    private final int mDefaultStrongAuthFlags = STRONG_AUTH_NOT_REQUIRED;
+    private final boolean mDefaultIsNonStrongBiometricAllowed = true;
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private LockSettingsStrongAuth.Injector mInjector;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private DevicePolicyManager mDPM;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mInjector.getAlarmManager(mContext)).thenReturn(mAlarmManager);
+        when(mInjector.getDefaultStrongAuthFlags(mContext)).thenReturn(mDefaultStrongAuthFlags);
+        when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDPM);
+
+        mStrongAuth = new LockSettingsStrongAuth(mContext, mInjector);
+    }
+
+    @Test
+    public void testScheduleNonStrongBiometricIdleTimeout() {
+        final long nextAlarmTime = 1000;
+        when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS))
+                .thenReturn(nextAlarmTime);
+        mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID);
+
+        waitForIdle();
+        NonStrongBiometricIdleTimeoutAlarmListener alarm = mStrongAuth
+                .mNonStrongBiometricIdleTimeoutAlarmListener.get(PRIMARY_USER_ID);
+        // verify that a new alarm for idle timeout is added for the user
+        assertNotNull(alarm);
+        // verify that the alarm is scheduled
+        verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm);
+    }
+
+    @Test
+    public void testSetIsNonStrongBiometricAllowed_disallowed() {
+        mStrongAuth.setIsNonStrongBiometricAllowed(false /* allowed */, PRIMARY_USER_ID);
+
+        waitForIdle();
+        // verify that unlocking with non-strong biometrics is not allowed
+        assertFalse(mStrongAuth.mIsNonStrongBiometricAllowedForUser
+                .get(PRIMARY_USER_ID, mDefaultIsNonStrongBiometricAllowed));
+    }
+
+    @Test
+    public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_fallbackTimeout() {
+        final long nextAlarmTime = 1000;
+        when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS))
+                .thenReturn(nextAlarmTime);
+        mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID);
+
+        waitForIdle();
+        NonStrongBiometricTimeoutAlarmListener alarm =
+                mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID);
+        // verify that a new alarm for fallback timeout is added for the user
+        assertNotNull(alarm);
+        // verify that the alarm is scheduled
+        verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm);
+    }
+
+    @Test
+    public void testRequireStrongAuth_nonStrongBiometric_fallbackTimeout() {
+        mStrongAuth.requireStrongAuth(
+                STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT /* strongAuthReason */,
+                PRIMARY_USER_ID);
+
+        waitForIdle();
+        // verify that the StrongAuthFlags for the user contains the expected flag
+        final int expectedFlag = STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
+        verifyStrongAuthFlags(expectedFlag, PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_cancelIdleTimeout() {
+        // lock device and schedule an alarm for non-strong biometric idle timeout
+        mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID);
+        // unlock with non-strong biometric
+        mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID);
+
+        waitForIdle();
+
+        // verify that the current alarm for idle timeout is cancelled after a successful unlock
+        verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class));
+    }
+
+    @Test
+    public void testReportSuccessfulBiometricUnlock_strongBio_cancelAlarmsAndAllowNonStrongBio() {
+        setupAlarms(PRIMARY_USER_ID);
+        mStrongAuth.reportSuccessfulBiometricUnlock(true /* isStrongBiometric */, PRIMARY_USER_ID);
+
+        waitForIdle();
+        // verify that unlocking with strong biometric cancels alarms for fallback and idle timeout
+        // and re-allow unlocking with non-strong biometric
+        verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportSuccessfulStrongAuthUnlock_schedulePrimaryAuthTimeout() {
+        final long nextAlarmTime = 1000;
+        when(mInjector.getNextAlarmTimeMs(mDPM.getRequiredStrongAuthTimeout(null, PRIMARY_USER_ID)))
+                .thenReturn(nextAlarmTime);
+        mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID);
+
+        waitForIdle();
+        StrongAuthTimeoutAlarmListener alarm =
+                mStrongAuth.mStrongAuthTimeoutAlarmListenerForUser.get(PRIMARY_USER_ID);
+        // verify that a new alarm for primary auth timeout is added for the user
+        assertNotNull(alarm);
+        // verify that the alarm is scheduled
+        verifyAlarm(nextAlarmTime, STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm);
+    }
+
+    @Test
+    public void testReportSuccessfulStrongAuthUnlock_cancelAlarmsAndAllowNonStrongBio() {
+        setupAlarms(PRIMARY_USER_ID);
+        mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID);
+
+        waitForIdle();
+        // verify that unlocking with primary auth (PIN/pattern/password) cancels alarms
+        // for fallback and idle timeout and re-allow unlocking with non-strong biometric
+        verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testFallbackTimeout_convenienceBiometric_weakBiometric() {
+        // assume that unlock with convenience biometric
+        mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID);
+        // assume that unlock again with weak biometric
+        mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID);
+
+        waitForIdle();
+        // verify that the fallback alarm scheduled when unlocking with convenience biometric is
+        // not affected when unlocking again with weak biometric
+        verify(mAlarmManager, never()).cancel(any(NonStrongBiometricTimeoutAlarmListener.class));
+        assertNotNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID));
+    }
+
+    private void verifyAlarm(long when, String tag, AlarmManager.OnAlarmListener alarm) {
+        verify(mAlarmManager).set(
+                eq(AlarmManager.ELAPSED_REALTIME),
+                eq(when),
+                eq(tag),
+                eq(alarm),
+                eq(mStrongAuth.mHandler));
+    }
+
+    private void verifyStrongAuthFlags(int reason, int userId) {
+        final int flags = mStrongAuth.mStrongAuthForUser.get(userId, mDefaultStrongAuthFlags);
+        Log.d(TAG, "verifyStrongAuthFlags:"
+                + " reason=" + Integer.toHexString(reason)
+                + " userId=" + userId
+                + " flags=" + Integer.toHexString(flags));
+        assertTrue(containsFlag(flags, reason));
+    }
+
+    private void setupAlarms(int userId) {
+        // schedule (a) an alarm for non-strong biometric fallback timeout and (b) an alarm for
+        // non-strong biometric idle timeout, so later we can verify that unlocking with
+        // strong biometric or primary auth will cancel those alarms
+        mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID);
+        mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID);
+    }
+
+    private void verifyAlarmsCancelledAndNonStrongBiometricAllowed(int userId) {
+        // verify that the current alarm for non-strong biometric fallback timeout is cancelled and
+        // removed
+        verify(mAlarmManager).cancel(any(NonStrongBiometricTimeoutAlarmListener.class));
+        assertNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(userId));
+
+        // verify that the current alarm for non-strong biometric idle timeout is cancelled
+        verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class));
+
+        // verify that unlocking with non-strong biometrics is allowed
+        assertTrue(mStrongAuth.mIsNonStrongBiometricAllowedForUser
+                .get(userId, mDefaultIsNonStrongBiometricAllowed));
+    }
+
+    private static boolean containsFlag(int haystack, int needle) {
+        return (haystack & needle) != 0;
+    }
+
+    private static void waitForIdle() {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
index 0a6d3f3..93ded1b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
@@ -1196,7 +1196,7 @@
         mDefaultTaskDisplayArea.registerStackOrderChangedListener(listener);
         try {
             mStack.mReparenting = true;
-            mDefaultTaskDisplayArea.addStack(mStack, 0);
+            mDefaultTaskDisplayArea.addChild(mStack, 0);
         } finally {
             mDefaultTaskDisplayArea.unregisterStackOrderChangedListener(listener);
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index 6ae8313..0700f9f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -329,6 +329,7 @@
         private boolean mCreateStack = true;
 
         private ActivityStack mStack;
+        private TaskDisplayArea mTaskDisplayArea;
 
         TaskBuilder(ActivityStackSupervisor supervisor) {
             mSupervisor = supervisor;
@@ -378,9 +379,16 @@
             return this;
         }
 
+        TaskBuilder setDisplay(DisplayContent display) {
+            mTaskDisplayArea = display.getDefaultTaskDisplayArea();
+            return this;
+        }
+
         Task build() {
             if (mStack == null && mCreateStack) {
-                mStack = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea().createStack(
+                TaskDisplayArea displayArea = mTaskDisplayArea != null ? mTaskDisplayArea
+                        : mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
+                mStack = displayArea.createStack(
                         WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
                 spyOn(mStack);
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index daff149..80fcf2e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -41,6 +41,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
+import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
@@ -74,6 +75,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 
 import android.annotation.SuppressLint;
+import android.app.ActivityTaskManager;
 import android.app.WindowConfiguration;
 import android.content.res.Configuration;
 import android.graphics.Rect;
@@ -1207,6 +1209,31 @@
         assertNull(taskDisplayArea.getOrCreateRootHomeTask());
     }
 
+    @Test
+    public void testFindScrollCaptureTargetWindow_behindWindow() {
+        DisplayContent display = createNewDisplay();
+        ActivityStack stack = createTaskStackOnDisplay(display);
+        Task task = createTaskInStack(stack, 0 /* userId */);
+        WindowState activityWindow = createAppWindow(task, TYPE_APPLICATION, "App Window");
+        WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
+
+        WindowState result = display.findScrollCaptureTargetWindow(behindWindow,
+                ActivityTaskManager.INVALID_TASK_ID);
+        assertEquals(activityWindow, result);
+    }
+
+    @Test
+    public void testFindScrollCaptureTargetWindow_taskId() {
+        DisplayContent display = createNewDisplay();
+        ActivityStack stack = createTaskStackOnDisplay(display);
+        Task task = createTaskInStack(stack, 0 /* userId */);
+        WindowState window = createAppWindow(task, TYPE_APPLICATION, "App Window");
+        WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot");
+
+        WindowState result = display.findScrollCaptureTargetWindow(null, task.mTaskId);
+        assertEquals(window, result);
+    }
+
     private boolean isOptionsPanelAtRight(int displayId) {
         return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT;
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index be25597..e887be0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
 /**
  * Build/Install/Run:
@@ -40,6 +41,7 @@
  */
 @SmallTest
 @Presubmit
+@RunWith(WindowTestRunner.class)
 @FlakyTest
 public class RefreshRatePolicyTest extends WindowTestsBase {
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
index 519ac78..dcc2ff1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
@@ -54,10 +54,7 @@
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager;
 import android.app.TaskInfo;
@@ -75,12 +72,10 @@
 import android.util.Xml;
 import android.view.DisplayInfo;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import com.android.internal.app.IVoiceInteractor;
 import com.android.server.wm.Task.TaskFactory;
-import com.android.server.wm.utils.WmDisplayCutout;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -368,25 +363,38 @@
 
     @Test
     public void testComputeConfigResourceOverrides() {
-        final Task task = new TaskBuilder(mSupervisor).build();
+        final Rect fullScreenBounds = new Rect(0, 0, 1080, 1920);
+        TestDisplayContent display = new TestDisplayContent.Builder(
+                mService, fullScreenBounds.width(), fullScreenBounds.height()).build();
+        final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build();
         final Configuration inOutConfig = new Configuration();
         final Configuration parentConfig = new Configuration();
         final int longSide = 1200;
         final int shortSide = 600;
+        final Rect parentBounds = new Rect(0, 0, 250, 500);
+        parentConfig.windowConfiguration.setBounds(parentBounds);
         parentConfig.densityDpi = 400;
-        parentConfig.screenHeightDp = 200; // 200 * 400 / 160 = 500px
-        parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px
+        parentConfig.screenHeightDp = (parentBounds.bottom * 160) / parentConfig.densityDpi; // 200
+        parentConfig.screenWidthDp = (parentBounds.right * 160) / parentConfig.densityDpi; // 100
         parentConfig.windowConfiguration.setRotation(ROTATION_0);
 
-        // Portrait bounds.
-        inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide);
-        // By default, the parent bounds should limit the existing input bounds.
+        // By default, the input bounds will fill parent.
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
         assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
         assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
         assertEquals(Configuration.ORIENTATION_PORTRAIT, inOutConfig.orientation);
 
+        // If bounds are overridden, config properties should be made to match. Surface hierarchy
+        // will crop for policy.
+        inOutConfig.setToDefaults();
+        inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide);
+        // By default, the parent bounds should limit the existing input bounds.
+        task.computeConfigResourceOverrides(inOutConfig, parentConfig);
+
+        assertEquals(longSide, inOutConfig.screenHeightDp * parentConfig.densityDpi / 160);
+        assertEquals(shortSide, inOutConfig.screenWidthDp * parentConfig.densityDpi / 160);
+
         inOutConfig.setToDefaults();
         // Landscape bounds.
         inOutConfig.windowConfiguration.getBounds().set(0, 0, longSide, shortSide);
@@ -394,21 +402,17 @@
         // Setup the display with a top stable inset. The later assertion will ensure the inset is
         // excluded from screenHeightDp.
         final int statusBarHeight = 100;
-        final DisplayContent displayContent = task.mDisplayContent;
-        final DisplayPolicy policy = mock(DisplayPolicy.class);
+        final DisplayPolicy policy = display.getDisplayPolicy();
         doAnswer(invocationOnMock -> {
             final Rect insets = invocationOnMock.<Rect>getArgument(0);
             insets.top = statusBarHeight;
             return null;
         }).when(policy).convertNonDecorInsetsToStableInsets(any(), eq(ROTATION_0));
-        doReturn(policy).when(displayContent).getDisplayPolicy();
-        doReturn(mock(WmDisplayCutout.class)).when(displayContent)
-                .calculateDisplayCutoutForRotation(anyInt());
 
         // Without limiting to be inside the parent bounds, the out screen size should keep relative
         // to the input bounds.
         final ActivityRecord.CompatDisplayInsets compatIntsets =
-                new ActivityRecord.CompatDisplayInsets(displayContent, task);
+                new ActivityRecord.CompatDisplayInsets(display, task);
         task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatIntsets);
 
         assertEquals((shortSide - statusBarHeight) * DENSITY_DEFAULT / parentConfig.densityDpi,
@@ -454,7 +458,6 @@
         parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px
         parentConfig.windowConfiguration.setRotation(ROTATION_0);
 
-        final float density = 2.5f; // densityDpi / DENSITY_DEFAULT_SCALE = 400 / 160.0f
         final int longSideDp = 480; // longSide / density = 1200 / 400 * 160
         final int shortSideDp = 240; // shortSide / density = 600 / 400 * 160
         final int screenLayout = parentConfig.screenLayout
@@ -463,31 +466,38 @@
                 Configuration.reduceScreenLayout(screenLayout, longSideDp, shortSideDp);
 
         // Portrait bounds overlapping with navigation bar, without insets.
-        inOutConfig.windowConfiguration.getBounds().set(0,
+        final Rect freeformBounds = new Rect(0,
                 displayHeight - 10 - longSide,
                 shortSide,
                 displayHeight - 10);
+        inOutConfig.windowConfiguration.setBounds(freeformBounds);
         // Set to freeform mode to verify bug fix.
         inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
 
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
-        assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
-        assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
+        // screenW/H should not be effected by parent since overridden and freeform
+        assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenWidthDp);
+        assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenHeightDp);
         assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
 
         inOutConfig.setToDefaults();
         // Landscape bounds overlapping with navigtion bar, without insets.
-        inOutConfig.windowConfiguration.getBounds().set(0,
+        freeformBounds.set(0,
                 displayHeight - 10 - shortSide,
                 longSide,
                 displayHeight - 10);
+        inOutConfig.windowConfiguration.setBounds(freeformBounds);
         inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
 
         task.computeConfigResourceOverrides(inOutConfig, parentConfig);
 
-        assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp);
-        assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp);
+        assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenWidthDp);
+        assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi,
+                inOutConfig.screenHeightDp);
         assertEquals(reducedScreenLayout, inOutConfig.screenLayout);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
index 7cb5e84..f354a04 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java
@@ -186,7 +186,7 @@
         final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent);
         final int stackOutset = 10;
         spyOn(stack);
-        doReturn(stackOutset).when(stack).getStackOutset();
+        doReturn(stackOutset).when(stack).getTaskOutset();
         doReturn(true).when(stack).inMultiWindowMode();
 
         // Mock the resolved override windowing mode to non-fullscreen
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 91c3c27..e39b4bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -24,6 +24,7 @@
 import android.util.MergedConfiguration;
 import android.view.DisplayCutout;
 import android.view.DragEvent;
+import android.view.IScrollCaptureController;
 import android.view.IWindow;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -113,6 +114,10 @@
     }
 
     @Override
+    public void requestScrollCapture(IScrollCaptureController controller) throws RemoteException {
+    }
+
+    @Override
     public void showInsets(int types, boolean fromIme) throws RemoteException {
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 53cc09b..f65328d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -44,7 +44,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -407,22 +406,20 @@
                 .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
         final Task task = stack.getTopMostTask();
         WindowContainerTransaction t = new WindowContainerTransaction();
-        t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 100, 100));
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
         final int origScreenWDp = task.getConfiguration().screenHeightDp;
         final int origScreenHDp = task.getConfiguration().screenHeightDp;
         t = new WindowContainerTransaction();
         // verify that setting config overrides on parent restricts children.
         t.setScreenSizeDp(stack.mRemoteToken
-                .toWindowContainerToken(), origScreenWDp, origScreenHDp);
-        t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 150, 200));
+                .toWindowContainerToken(), origScreenWDp, origScreenHDp / 2);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
-        assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
+        assertEquals(origScreenHDp / 2, task.getConfiguration().screenHeightDp);
         t = new WindowContainerTransaction();
         t.setScreenSizeDp(stack.mRemoteToken.toWindowContainerToken(), SCREEN_WIDTH_DP_UNDEFINED,
                 SCREEN_HEIGHT_DP_UNDEFINED);
         mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
-        assertNotEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
+        assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index e561c13..6a64d1c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -247,7 +247,7 @@
     WindowState createAppWindow(Task task, int type, String name) {
         synchronized (mWm.mGlobalLock) {
             final ActivityRecord activity =
-                    WindowTestUtils.createTestActivityRecord(mDisplayContent);
+                    WindowTestUtils.createTestActivityRecord(task.getDisplayContent());
             task.addChild(activity, 0);
             return createWindow(null, type, activity, name);
         }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index bbe9851..5b5d57bf 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -1577,15 +1577,27 @@
         }
 
         @Override
-        public boolean isAppInactive(String packageName, int userId) {
+        public boolean isAppInactive(String packageName, int userId, String callingPackage) {
+            final int callingUid = Binder.getCallingUid();
             try {
                 userId = ActivityManager.getService().handleIncomingUser(Binder.getCallingPid(),
-                        Binder.getCallingUid(), userId, false, false, "isAppInactive", null);
+                        callingUid, userId, false, false, "isAppInactive", null);
             } catch (RemoteException re) {
                 throw re.rethrowFromSystemServer();
             }
+
+            // If the calling app is asking about itself, continue, else check for permission.
+            if (packageName.equals(callingPackage)) {
+                final int actualCallingUid = mPackageManagerInternal.getPackageUidInternal(
+                        callingPackage, 0, userId);
+                if (actualCallingUid != callingUid) {
+                    return false;
+                }
+            } else if (!hasPermission(callingPackage)) {
+                return false;
+            }
             final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller(
-                    Binder.getCallingUid(), userId);
+                    callingUid, userId);
             final long token = Binder.clearCallingIdentity();
             try {
                 return mAppStandby.isAppIdleFiltered(
diff --git a/telephony/java/com/android/internal/telephony/SmsHeader.java b/telephony/java/com/android/internal/telephony/SmsHeader.java
index ab3fdf4..2f3897b 100644
--- a/telephony/java/com/android/internal/telephony/SmsHeader.java
+++ b/telephony/java/com/android/internal/telephony/SmsHeader.java
@@ -23,6 +23,8 @@
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * SMS user data header, as specified in TS 23.040 9.2.3.24.
@@ -71,6 +73,25 @@
     public static final int PORT_WAP_PUSH = 2948;
     public static final int PORT_WAP_WSP  = 9200;
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SmsHeader smsHeader = (SmsHeader) o;
+        return languageTable == smsHeader.languageTable
+                && languageShiftTable == smsHeader.languageShiftTable
+                && Objects.equals(portAddrs, smsHeader.portAddrs)
+                && Objects.equals(concatRef, smsHeader.concatRef)
+                && Objects.equals(specialSmsMsgList, smsHeader.specialSmsMsgList)
+                && Objects.equals(miscEltList, smsHeader.miscEltList);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(portAddrs, concatRef, specialSmsMsgList, miscEltList, languageTable,
+                languageShiftTable);
+    }
+
     public static class PortAddrs {
         @UnsupportedAppUsage
         public PortAddrs() {
@@ -81,6 +102,21 @@
         @UnsupportedAppUsage
         public int origPort;
         public boolean areEightBits;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            PortAddrs portAddrs = (PortAddrs) o;
+            return destPort == portAddrs.destPort
+                    && origPort == portAddrs.origPort
+                    && areEightBits == portAddrs.areEightBits;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(destPort, origPort, areEightBits);
+        }
     }
 
     public static class ConcatRef {
@@ -95,11 +131,41 @@
         @UnsupportedAppUsage
         public int msgCount;
         public boolean isEightBits;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ConcatRef concatRef = (ConcatRef) o;
+            return refNumber == concatRef.refNumber
+                    && seqNumber == concatRef.seqNumber
+                    && msgCount == concatRef.msgCount
+                    && isEightBits == concatRef.isEightBits;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(refNumber, seqNumber, msgCount, isEightBits);
+        }
     }
 
     public static class SpecialSmsMsg {
         public int msgIndType;
         public int msgCount;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            SpecialSmsMsg that = (SpecialSmsMsg) o;
+            return msgIndType == that.msgIndType
+                    && msgCount == that.msgCount;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(msgIndType, msgCount);
+        }
     }
 
     /**
@@ -109,6 +175,22 @@
     public static class MiscElt {
         public int id;
         public byte[] data;
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            MiscElt miscElt = (MiscElt) o;
+            return id == miscElt.id
+                    && Arrays.equals(data, miscElt.data);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(id);
+            result = 31 * result + Arrays.hashCode(data);
+            return result;
+        }
     }
 
     @UnsupportedAppUsage
diff --git a/tests/AutoVerify/app1/Android.bp b/tests/AutoVerify/app1/Android.bp
new file mode 100644
index 0000000..548519f
--- /dev/null
+++ b/tests/AutoVerify/app1/Android.bp
@@ -0,0 +1,11 @@
+android_app {
+    name: "AutoVerifyTest",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    min_sdk_version: "26",
+    target_sdk_version: "26",
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/tests/AutoVerify/app1/AndroidManifest.xml b/tests/AutoVerify/app1/AndroidManifest.xml
new file mode 100644
index 0000000..d9caad4
--- /dev/null
+++ b/tests/AutoVerify/app1/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.autoverify" >
+
+    <uses-sdk android:targetSdkVersion="26" />
+
+    <application
+        android:label="@string/app_name" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="explicit.example.com" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/AutoVerify/app1/res/values/strings.xml b/tests/AutoVerify/app1/res/values/strings.xml
new file mode 100644
index 0000000..e234355
--- /dev/null
+++ b/tests/AutoVerify/app1/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 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.
+-->
+
+<resources>
+    <!-- app icon label, do not translate -->
+    <string name="app_name" translatable="false">AutoVerify Test</string>
+</resources>
diff --git a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java
new file mode 100644
index 0000000..09ef472
--- /dev/null
+++ b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
diff --git a/tests/AutoVerify/app2/Android.bp b/tests/AutoVerify/app2/Android.bp
new file mode 100644
index 0000000..1c6c97b
--- /dev/null
+++ b/tests/AutoVerify/app2/Android.bp
@@ -0,0 +1,11 @@
+android_app {
+    name: "AutoVerifyTest2",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    min_sdk_version: "26",
+    target_sdk_version: "26",
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/tests/AutoVerify/app2/AndroidManifest.xml b/tests/AutoVerify/app2/AndroidManifest.xml
new file mode 100644
index 0000000..a008078
--- /dev/null
+++ b/tests/AutoVerify/app2/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.autoverify" >
+
+    <uses-sdk android:targetSdkVersion="26" />
+
+    <application
+        android:label="@string/app_name" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="explicit.example.com" />
+                <data android:host="*.wildcard.tld" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/AutoVerify/app2/res/values/strings.xml b/tests/AutoVerify/app2/res/values/strings.xml
new file mode 100644
index 0000000..e234355
--- /dev/null
+++ b/tests/AutoVerify/app2/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 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.
+-->
+
+<resources>
+    <!-- app icon label, do not translate -->
+    <string name="app_name" translatable="false">AutoVerify Test</string>
+</resources>
diff --git a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java
new file mode 100644
index 0000000..09ef472
--- /dev/null
+++ b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
diff --git a/tests/AutoVerify/app3/Android.bp b/tests/AutoVerify/app3/Android.bp
new file mode 100644
index 0000000..70a2b77
--- /dev/null
+++ b/tests/AutoVerify/app3/Android.bp
@@ -0,0 +1,11 @@
+android_app {
+    name: "AutoVerifyTest3",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    min_sdk_version: "26",
+    target_sdk_version: "26",
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/tests/AutoVerify/app3/AndroidManifest.xml b/tests/AutoVerify/app3/AndroidManifest.xml
new file mode 100644
index 0000000..efaabc9
--- /dev/null
+++ b/tests/AutoVerify/app3/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.autoverify" >
+
+    <uses-sdk android:targetSdkVersion="26" />
+
+    <application
+        android:label="@string/app_name" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+            <!-- does not request autoVerify -->
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="explicit.example.com" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/AutoVerify/app3/res/values/strings.xml b/tests/AutoVerify/app3/res/values/strings.xml
new file mode 100644
index 0000000..e234355
--- /dev/null
+++ b/tests/AutoVerify/app3/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 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.
+-->
+
+<resources>
+    <!-- app icon label, do not translate -->
+    <string name="app_name" translatable="false">AutoVerify Test</string>
+</resources>
diff --git a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java
new file mode 100644
index 0000000..09ef472
--- /dev/null
+++ b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
diff --git a/tests/AutoVerify/app4/Android.bp b/tests/AutoVerify/app4/Android.bp
new file mode 100644
index 0000000..fbdae11
--- /dev/null
+++ b/tests/AutoVerify/app4/Android.bp
@@ -0,0 +1,11 @@
+android_app {
+    name: "AutoVerifyTest4",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    min_sdk_version: "26",
+    target_sdk_version: "26",
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/tests/AutoVerify/app4/AndroidManifest.xml b/tests/AutoVerify/app4/AndroidManifest.xml
new file mode 100644
index 0000000..1c975f8
--- /dev/null
+++ b/tests/AutoVerify/app4/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.test.autoverify" >
+
+    <uses-sdk android:targetSdkVersion="26" />
+
+    <application
+        android:label="@string/app_name" >
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+            <!-- intentionally does not autoVerify -->
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="explicit.example.com" />
+                <data android:host="*.wildcard.tld" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/AutoVerify/app4/res/values/strings.xml b/tests/AutoVerify/app4/res/values/strings.xml
new file mode 100644
index 0000000..e234355
--- /dev/null
+++ b/tests/AutoVerify/app4/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 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.
+-->
+
+<resources>
+    <!-- app icon label, do not translate -->
+    <string name="app_name" translatable="false">AutoVerify Test</string>
+</resources>
diff --git a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java
new file mode 100644
index 0000000..09ef472
--- /dev/null
+++ b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 6c8dc00..7d20d0d 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -758,6 +758,13 @@
     @SystemApi
     public static final int SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS = 1;
 
+    /**
+     * Client disconnected for unspecified reason. This could for example be because the AP is being
+     * shut down.
+     * @hide
+     */
+    public static final int SAP_CLIENT_DISCONNECT_REASON_CODE_UNSPECIFIED = 2;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"IFACE_IP_MODE_"}, value = {