Merge "Update default for conversations in DND" into rvc-dev
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/api/test-current.txt b/api/test-current.txt
index 8f3d042..755380e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -151,6 +151,7 @@
     ctor public ActivityView(android.content.Context, android.util.AttributeSet);
     ctor public ActivityView(android.content.Context, android.util.AttributeSet, int);
     ctor public ActivityView(android.content.Context, android.util.AttributeSet, int, boolean);
+    ctor public ActivityView(@NonNull android.content.Context, @NonNull android.util.AttributeSet, int, boolean, boolean);
     method public int getVirtualDisplayId();
     method public void onLayout(boolean, int, int, int, int);
     method public void onLocationChanged();
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/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java
index ab7925c..1098fa1 100644
--- a/core/java/android/app/ActivityView.java
+++ b/core/java/android/app/ActivityView.java
@@ -90,11 +90,23 @@
 
     public ActivityView(Context context, AttributeSet attrs, int defStyle,
             boolean singleTaskInstance) {
+        this(context, attrs, defStyle, singleTaskInstance, false /* usePublicVirtualDisplay */);
+    }
+
+    /**
+     * This constructor let's the caller explicitly request a public virtual display as the backing
+     * display. Using a public display is not recommended as it exposes it to other applications,
+     * but it might be needed for backwards compatibility.
+     */
+    public ActivityView(
+            @NonNull Context context, @NonNull AttributeSet attrs, int defStyle,
+            boolean singleTaskInstance, boolean usePublicVirtualDisplay) {
         super(context, attrs, defStyle);
         if (useTaskOrganizer()) {
             mTaskEmbedder = new TaskOrganizerTaskEmbedder(context, this);
         } else {
-            mTaskEmbedder = new VirtualDisplayTaskEmbedder(context, this, singleTaskInstance);
+            mTaskEmbedder = new VirtualDisplayTaskEmbedder(context, this, singleTaskInstance,
+                    usePublicVirtualDisplay);
         }
         mSurfaceView = new SurfaceView(context);
         // Since ActivityView#getAlpha has been overridden, we should use parent class's alpha
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/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/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..68a185d 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) {
@@ -4778,6 +4788,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 +5091,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 +8803,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 +9230,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/android/window/VirtualDisplayTaskEmbedder.java b/core/java/android/window/VirtualDisplayTaskEmbedder.java
index 7016469..1c0598b 100644
--- a/core/java/android/window/VirtualDisplayTaskEmbedder.java
+++ b/core/java/android/window/VirtualDisplayTaskEmbedder.java
@@ -64,6 +64,7 @@
     // For Virtual Displays
     private int mDisplayDensityDpi;
     private final boolean mSingleTaskInstance;
+    private final boolean mUsePublicVirtualDisplay;
     private VirtualDisplay mVirtualDisplay;
     private Insets mForwardedInsets;
     private DisplayMetrics mTmpDisplayMetrics;
@@ -78,9 +79,10 @@
      *                           only applicable if virtual displays are used
      */
     public VirtualDisplayTaskEmbedder(Context context, VirtualDisplayTaskEmbedder.Host host,
-            boolean singleTaskInstance) {
+            boolean singleTaskInstance, boolean usePublicVirtualDisplay) {
         super(context, host);
         mSingleTaskInstance = singleTaskInstance;
+        mUsePublicVirtualDisplay = usePublicVirtualDisplay;
     }
 
     /**
@@ -97,11 +99,16 @@
     public boolean onInitialize() {
         final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
         mDisplayDensityDpi = getBaseDisplayDensity();
+
+        int virtualDisplayFlags = VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
+                | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
+        if (mUsePublicVirtualDisplay) {
+            virtualDisplayFlags |= VIRTUAL_DISPLAY_FLAG_PUBLIC;
+        }
+
         mVirtualDisplay = displayManager.createVirtualDisplay(
                 DISPLAY_NAME + "@" + System.identityHashCode(this), mHost.getWidth(),
-                mHost.getHeight(), mDisplayDensityDpi, null,
-                VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
-                        | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL);
+                mHost.getHeight(), mDisplayDensityDpi, null, virtualDisplayFlags);
 
         if (mVirtualDisplay == null) {
             Log.e(TAG, "Failed to initialize TaskEmbedder");
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/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/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..3af0533
--- /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/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/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/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/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/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/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/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..ea358c7 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);
                 }
             }
 
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/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/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/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/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/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/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index f1ea5d0..552331e 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -5889,6 +5889,7 @@
 
     private void processLinkPropertiesFromAgent(NetworkAgentInfo nai, LinkProperties lp) {
         lp.ensureDirectlyConnectedRoutes();
+        nai.clatd.setNat64PrefixFromRa(lp.getNat64Prefix());
     }
 
     private void updateLinkProperties(NetworkAgentInfo networkAgent, LinkProperties newLp,
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/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index cce749d..e28464a 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -2240,10 +2240,11 @@
             if (mVoldAppDataIsolationEnabled && UserHandle.isApp(app.uid)
                     && !storageManagerInternal.isExternalStorageService(uid)) {
                 bindMountAppStorageDirs = true;
-                if (!storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
+                if (pkgDataInfoMap == null ||
+                        !storageManagerInternal.prepareStorageDirs(userId, pkgDataInfoMap.keySet(),
                         app.processName)) {
-                    // Cannot prepare Android/app and Android/obb directory,
-                    // so we won't mount it in zygote.
+                    // Cannot prepare Android/app and Android/obb directory or inode == 0,
+                    // so we won't mount it in zygote, but resume the mount after unlocking device.
                     app.bindMountPending = true;
                     bindMountAppStorageDirs = false;
                 }
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 97d3b17..1139fb2 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -5750,7 +5750,7 @@
      */
     private void switchPackageIfBootTimeOrRarelyUsedLocked(@NonNull String packageName) {
         if (mSampledPackage == null) {
-            if (ThreadLocalRandom.current().nextFloat() < 0.1f) {
+            if (ThreadLocalRandom.current().nextFloat() < 0.5f) {
                 mSamplingStrategy = SAMPLING_STRATEGY_BOOT_TIME_SAMPLING;
                 resampleAppOpForPackageLocked(packageName);
             }
diff --git a/services/core/java/com/android/server/connectivity/Nat464Xlat.java b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
index e6b2d26..34d0bed 100644
--- a/services/core/java/com/android/server/connectivity/Nat464Xlat.java
+++ b/services/core/java/com/android/server/connectivity/Nat464Xlat.java
@@ -81,15 +81,23 @@
         RUNNING,      // start() called, and the stacked iface is known to be up.
     }
 
-    /** NAT64 prefix currently in use. Only valid in STARTING or RUNNING states. */
+    /**
+     * NAT64 prefix currently in use. Only valid in STARTING or RUNNING states.
+     * Used, among other things, to avoid updates when switching from a prefix learned from one
+     * source (e.g., RA) to the same prefix learned from another source (e.g., RA).
+     */
     private IpPrefix mNat64PrefixInUse;
     /** NAT64 prefix (if any) discovered from DNS via RFC 7050. */
     private IpPrefix mNat64PrefixFromDns;
+    /** NAT64 prefix (if any) learned from the network via RA. */
+    private IpPrefix mNat64PrefixFromRa;
     private String mBaseIface;
     private String mIface;
     private Inet6Address mIPv6Address;
     private State mState = State.IDLE;
 
+    private boolean mPrefixDiscoveryRunning;
+
     public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver,
             INetworkManagementService nmService) {
         mDnsResolver = dnsResolver;
@@ -139,15 +147,6 @@
     }
 
     /**
-     * @return true if we have started prefix discovery and not yet stopped it (regardless of
-     * whether it is still running or has succeeded).
-     * A true result corresponds to internal states DISCOVERING, STARTING and RUNNING.
-     */
-    public boolean isPrefixDiscoveryStarted() {
-        return mState == State.DISCOVERING || isStarted();
-    }
-
-    /**
      * @return true if clatd has been started and has not yet stopped.
      * A true result corresponds to internal states STARTING and RUNNING.
      */
@@ -181,7 +180,7 @@
             return;
         }
 
-        mNat64PrefixInUse = getNat64Prefix();
+        mNat64PrefixInUse = selectNat64Prefix();
         String addrStr = null;
         try {
             addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
@@ -196,6 +195,9 @@
         } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
             Slog.e(TAG, "Invalid IPv6 address " + addrStr);
         }
+        if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) {
+            stopPrefixDiscovery();
+        }
     }
 
     /**
@@ -218,10 +220,15 @@
         mNat64PrefixInUse = null;
         mIface = null;
         mBaseIface = null;
-        if (requiresClat(mNetwork)) {
+
+        if (isPrefixDiscoveryNeeded()) {
+            if (!mPrefixDiscoveryRunning) {
+                startPrefixDiscovery();
+            }
             mState = State.DISCOVERING;
         } else {
-            stopPrefixDiscovery();  // Enters IDLE state.
+            stopPrefixDiscovery();
+            mState = State.IDLE;
         }
     }
 
@@ -282,7 +289,7 @@
         } catch (RemoteException | ServiceSpecificException e) {
             Slog.e(TAG, "Error starting prefix discovery on netId " + getNetId() + ": " + e);
         }
-        mState = State.DISCOVERING;
+        mPrefixDiscoveryRunning = true;
     }
 
     private void stopPrefixDiscovery() {
@@ -291,11 +298,18 @@
         } catch (RemoteException | ServiceSpecificException e) {
             Slog.e(TAG, "Error stopping prefix discovery on netId " + getNetId() + ": " + e);
         }
-        mState = State.IDLE;
+        mPrefixDiscoveryRunning = false;
+    }
+
+    private boolean isPrefixDiscoveryNeeded() {
+        // If there is no NAT64 prefix in the RA, prefix discovery is always needed. It cannot be
+        // stopped after it succeeds, because stopping it will cause netd to report that the prefix
+        // has been removed, and that will cause us to stop clatd.
+        return requiresClat(mNetwork) && mNat64PrefixFromRa == null;
     }
 
     private void maybeHandleNat64PrefixChange() {
-        final IpPrefix newPrefix = getNat64Prefix();
+        final IpPrefix newPrefix = selectNat64Prefix();
         if (!Objects.equals(mNat64PrefixInUse, newPrefix)) {
             Slog.d(TAG, "NAT64 prefix changed from " + mNat64PrefixInUse + " to "
                     + newPrefix);
@@ -314,13 +328,11 @@
         // TODO: turn this class into a proper StateMachine. http://b/126113090
         switch (mState) {
             case IDLE:
-                if (requiresClat(mNetwork)) {
-                    // Network is detected to be IPv6-only.
-                    // TODO: consider going to STARTING directly if the NAT64 prefix is already
-                    // known. This would however result in clatd running without prefix discovery
-                    // running, which might be a surprising combination.
+                if (isPrefixDiscoveryNeeded()) {
                     startPrefixDiscovery();  // Enters DISCOVERING state.
-                    return;
+                    mState = State.DISCOVERING;
+                } else if (requiresClat(mNetwork)) {
+                    start();  // Enters STARTING state.
                 }
                 break;
 
@@ -333,6 +345,7 @@
                 if (!requiresClat(mNetwork)) {
                     // IPv4 address added. Go back to IDLE state.
                     stopPrefixDiscovery();
+                    mState = State.IDLE;
                     return;
                 }
                 break;
@@ -351,8 +364,20 @@
         }
     }
 
-    private IpPrefix getNat64Prefix() {
-        return mNat64PrefixFromDns;
+    /**
+     * Picks a NAT64 prefix to use. Always prefers the prefix from the RA if one is received from
+     * both RA and DNS, because the prefix in the RA has better security and updatability, and will
+     * almost always be received first anyway.
+     *
+     * Any network that supports legacy hosts will support discovering the DNS64 prefix via DNS as
+     * well. If the prefix from the RA is withdrawn, fall back to that for reliability purposes.
+     */
+    private IpPrefix selectNat64Prefix() {
+        return mNat64PrefixFromRa != null ? mNat64PrefixFromRa : mNat64PrefixFromDns;
+    }
+
+    public void setNat64PrefixFromRa(IpPrefix prefix) {
+        mNat64PrefixFromRa = prefix;
     }
 
     public void setNat64PrefixFromDns(IpPrefix prefix) {
@@ -367,7 +392,7 @@
     public void fixupLinkProperties(@NonNull LinkProperties oldLp, @NonNull LinkProperties lp) {
         // This must be done even if clatd is not running, because otherwise shouldStartClat would
         // never return true.
-        lp.setNat64Prefix(getNat64Prefix());
+        lp.setNat64Prefix(selectNat64Prefix());
 
         if (!isRunning()) {
             return;
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/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/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 07078f2..9f543b4 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1346,13 +1346,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 +17632,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 +18450,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 +19543,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 +19559,8 @@
                 }
             }
         } else {
-            if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) {
+            if (mSettings.removeIntentFilterVerificationLPw(packageName, userId,
+                    alsoResetStatus)) {
                 scheduleWritePackageRestrictionsLocked(userId);
             }
         }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 53c057a..44a61d8 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/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/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/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a47cdc6..b01acca 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5450,6 +5450,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/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/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/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/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/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/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..5b5d57b 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/tests/net/common/java/android/net/NetworkProviderTest.kt b/tests/net/common/java/android/net/NetworkProviderTest.kt
index 4601c4b..b7c47c2 100644
--- a/tests/net/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/net/common/java/android/net/NetworkProviderTest.kt
@@ -105,14 +105,43 @@
                 .build()
         val cb = ConnectivityManager.NetworkCallback()
         mCm.requestNetwork(nr, cb)
-        provider.expectCallback<OnNetworkRequested>() {
-            callback -> callback.request.getNetworkSpecifier() == specifier &&
+        provider.expectCallback<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
             callback.request.hasTransport(TRANSPORT_TEST)
         }
 
+        val initialScore = 40
+        val updatedScore = 60
+        val nc = NetworkCapabilities().apply {
+                addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+                removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                setNetworkSpecifier(specifier)
+        }
+        val lp = LinkProperties()
+        val config = NetworkAgentConfig.Builder().build()
+        val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc, lp,
+                initialScore, config, provider) {}
+
+        provider.expectCallback<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.score == initialScore &&
+            callback.id == agent.providerId
+        }
+
+        agent.sendNetworkScore(updatedScore)
+        provider.expectCallback<OnNetworkRequested>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
+            callback.score == updatedScore &&
+            callback.id == agent.providerId
+        }
+
         mCm.unregisterNetworkCallback(cb)
-        provider.expectCallback<OnNetworkRequestWithdrawn>() {
-            callback -> callback.request.getNetworkSpecifier() == specifier &&
+        provider.expectCallback<OnNetworkRequestWithdrawn>() { callback ->
+            callback.request.getNetworkSpecifier() == specifier &&
             callback.request.hasTransport(TRANSPORT_TEST)
         }
         mCm.unregisterNetworkProvider(provider)
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index dad0363..a478e68 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -6179,6 +6179,111 @@
         mCm.unregisterNetworkCallback(networkCallback);
     }
 
+    private void expectNat64PrefixChange(TestableNetworkCallback callback,
+            TestNetworkAgentWrapper agent, IpPrefix prefix) {
+        callback.expectLinkPropertiesThat(agent, x -> Objects.equals(x.getNat64Prefix(), prefix));
+    }
+
+    @Test
+    public void testNat64PrefixMultipleSources() throws Exception {
+        final String iface = "wlan0";
+        final String pref64FromRaStr = "64:ff9b::";
+        final String pref64FromDnsStr = "2001:db8:64::";
+        final IpPrefix pref64FromRa = new IpPrefix(InetAddress.getByName(pref64FromRaStr), 96);
+        final IpPrefix pref64FromDns = new IpPrefix(InetAddress.getByName(pref64FromDnsStr), 96);
+        final IpPrefix newPref64FromRa = new IpPrefix("2001:db8:64:64:64:64::/96");
+
+        final NetworkRequest request = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .build();
+        final TestNetworkCallback callback = new TestNetworkCallback();
+        mCm.registerNetworkCallback(request, callback);
+
+        final LinkProperties baseLp = new LinkProperties();
+        baseLp.setInterfaceName(iface);
+        baseLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+        baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464"));
+
+        reset(mMockNetd, mMockDnsResolver);
+        InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
+
+        // If a network already has a NAT64 prefix on connect, clatd is started immediately and
+        // prefix discovery is never started.
+        LinkProperties lp = new LinkProperties(baseLp);
+        lp.setNat64Prefix(pref64FromRa);
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+        mCellNetworkAgent.connect(false);
+        final Network network = mCellNetworkAgent.getNetwork();
+        int netId = network.getNetId();
+        callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+        callback.assertNoCallback();
+        assertEquals(pref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
+
+        // If the RA prefix is withdrawn, clatd is stopped and prefix discovery is started.
+        lp.setNat64Prefix(null);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, null);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
+
+        // If the RA prefix appears while DNS discovery is in progress, discovery is stopped and
+        // clatd is started with the prefix from the RA.
+        lp.setNat64Prefix(pref64FromRa);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromRa);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+
+        // Withdraw the RA prefix so we can test the case where an RA prefix appears after DNS
+        // discovery has succeeded.
+        lp.setNat64Prefix(null);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, null);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId);
+
+        mService.mNetdEventCallback.onNat64PrefixEvent(netId, true /* added */,
+                pref64FromDnsStr, 96);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromDns);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString());
+
+        // If the RA prefix reappears, clatd is restarted and prefix discovery is stopped.
+        lp.setNat64Prefix(pref64FromRa);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, pref64FromRa);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+        // If the RA prefix changes, clatd is restarted and prefix discovery is not started.
+        lp.setNat64Prefix(newPref64FromRa);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        expectNat64PrefixChange(callback, mCellNetworkAgent, newPref64FromRa);
+        inOrder.verify(mMockNetd).clatdStop(iface);
+        inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+        // If the RA prefix changes to the same value, nothing happens.
+        lp.setNat64Prefix(newPref64FromRa);
+        mCellNetworkAgent.sendLinkProperties(lp);
+        callback.assertNoCallback();
+        assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix());
+        inOrder.verify(mMockNetd, never()).clatdStop(iface);
+        inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString());
+        inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId);
+        inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId);
+
+        // The transition between no prefix and DNS prefix is tested in testStackedLinkProperties.
+
+        callback.assertNoCallback();
+        mCellNetworkAgent.disconnect();
+        mCm.unregisterNetworkCallback(callback);
+    }
+
     @Test
     public void testDataActivityTracking() throws Exception {
         final TestNetworkCallback networkCallback = new TestNetworkCallback();
diff --git a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
index d0ebb52..5046b65 100644
--- a/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
+++ b/tests/net/java/com/android/server/connectivity/Nat464XlatTest.java
@@ -58,8 +58,10 @@
 
     static final String BASE_IFACE = "test0";
     static final String STACKED_IFACE = "v4-test0";
+    static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64");
     static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29");
     static final String NAT64_PREFIX = "64:ff9b::/96";
+    static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96";
     static final int NETID = 42;
 
     @Mock ConnectivityService mConnectivity;
@@ -81,6 +83,14 @@
         };
     }
 
+    private void markNetworkConnected() {
+        mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", "");
+    }
+
+    private void markNetworkDisconnected() {
+        mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, "", "");
+    }
+
     @Before
     public void setUp() throws Exception {
         mLooper = new TestLooper();
@@ -92,6 +102,7 @@
         mNai.linkProperties.setInterfaceName(BASE_IFACE);
         mNai.networkInfo = new NetworkInfo(null);
         mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI);
+        markNetworkConnected();
         when(mNai.connService()).thenReturn(mConnectivity);
         when(mNai.netAgentConfig()).thenReturn(mAgentConfig);
         when(mNai.handler()).thenReturn(mHandler);
@@ -139,7 +150,7 @@
             for (NetworkInfo.DetailedState state : supportedDetailedStates) {
                 mNai.networkInfo.setDetailedState(state, "reason", "extraInfo");
 
-                mNai.linkProperties.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
+                mNai.linkProperties.setNat64Prefix(new IpPrefix(OTHER_NAT64_PREFIX));
                 assertRequiresClat(false, mNai);
                 assertShouldStartClat(false, mNai);
 
@@ -176,11 +187,20 @@
         }
     }
 
-    @Test
-    public void testNormalStartAndStop() throws Exception {
+    private void makeClatUnnecessary(boolean dueToDisconnect) {
+        if (dueToDisconnect) {
+            markNetworkDisconnected();
+        } else {
+            mNai.linkProperties.addLinkAddress(ADDR);
+        }
+    }
+
+    private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception {
         Nat464Xlat nat = makeNat464Xlat();
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
 
+        mNai.linkProperties.addLinkAddress(V6ADDR);
+
         nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
 
         // Start clat.
@@ -200,6 +220,7 @@
         assertRunning(nat);
 
         // Stop clat (Network disconnects, IPv4 addr appears, ...).
+        makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
         verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -217,11 +238,23 @@
         verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
     }
 
+    @Test
+    public void testNormalStartAndStopDueToDisconnect() throws Exception {
+        checkNormalStartAndStop(true);
+    }
+
+    @Test
+    public void testNormalStartAndStopDueToIpv4Addr() throws Exception {
+        checkNormalStartAndStop(false);
+    }
+
     private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception {
         Nat464Xlat nat = makeNat464Xlat();
         ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class);
         InOrder inOrder = inOrder(mNetd, mConnectivity);
 
+        mNai.linkProperties.addLinkAddress(V6ADDR);
+
         nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
 
         nat.start();
@@ -344,10 +377,11 @@
         verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
     }
 
-    @Test
-    public void testStopBeforeClatdStarts() throws Exception {
+    private void checkStopBeforeClatdStarts(boolean dueToDisconnect) throws Exception {
         Nat464Xlat nat = makeNat464Xlat();
 
+        mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
         nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
 
         nat.start();
@@ -356,6 +390,7 @@
         verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+        makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
         verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -377,9 +412,20 @@
     }
 
     @Test
-    public void testStopAndClatdNeverStarts() throws Exception {
+    public void testStopDueToDisconnectBeforeClatdStarts() throws Exception {
+        checkStopBeforeClatdStarts(true);
+    }
+
+    @Test
+    public void testStopDueToIpv4AddrBeforeClatdStarts() throws Exception {
+        checkStopBeforeClatdStarts(false);
+    }
+
+    private void checkStopAndClatdNeverStarts(boolean dueToDisconnect) throws Exception {
         Nat464Xlat nat = makeNat464Xlat();
 
+        mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+
         nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX));
 
         nat.start();
@@ -388,6 +434,7 @@
         verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX));
 
         // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...)
+        makeClatUnnecessary(dueToDisconnect);
         nat.stop();
 
         verify(mNetd).clatdStop(eq(BASE_IFACE));
@@ -398,6 +445,57 @@
         verifyNoMoreInteractions(mNetd, mNms, mConnectivity);
     }
 
+    @Test
+    public void testStopDueToDisconnectAndClatdNeverStarts() throws Exception {
+        checkStopAndClatdNeverStarts(true);
+    }
+
+    @Test
+    public void testStopDueToIpv4AddressAndClatdNeverStarts() throws Exception {
+        checkStopAndClatdNeverStarts(false);
+    }
+
+    @Test
+    public void testNat64PrefixPreference() throws Exception {
+        final IpPrefix prefixFromDns = new IpPrefix(NAT64_PREFIX);
+        final IpPrefix prefixFromRa = new IpPrefix(OTHER_NAT64_PREFIX);
+
+        Nat464Xlat nat = makeNat464Xlat();
+
+        final LinkProperties emptyLp = new LinkProperties();
+        LinkProperties fixedupLp;
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromDns(prefixFromDns);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(prefixFromRa);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromDns, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(prefixFromRa);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromDns(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(prefixFromRa, fixedupLp.getNat64Prefix());
+
+        fixedupLp = new LinkProperties();
+        nat.setNat64PrefixFromRa(null);
+        nat.fixupLinkProperties(emptyLp, fixedupLp);
+        assertEquals(null, fixedupLp.getNat64Prefix());
+    }
+
     static void assertIdle(Nat464Xlat nat) {
         assertTrue("Nat464Xlat was not IDLE", !nat.isStarted());
     }