Merge "Support BP retry face auth on SFPS acquired" into 24D1-dev
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 48ddd30..81a1a2d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -12956,7 +12956,7 @@
     field public static final String SERVICE_INTERFACE = "android.service.ondeviceintelligence.OnDeviceIntelligenceService";
   }
 
-  public static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception {
+  public abstract static class OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException extends java.lang.Exception {
     ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int);
     ctor public OnDeviceIntelligenceService.OnDeviceIntelligenceServiceException(int, @NonNull String);
     method public int getErrorCode();
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 03fa841..8439833 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10919,7 +10919,7 @@
                 "biometric_debug_enabled";
 
         /**
-         * Whether or not virtual sensors are enabled.
+         * Whether or not both fingerprint and face virtual sensors are enabled.
          * @hide
          */
         @TestApi
@@ -10927,6 +10927,22 @@
         public static final String BIOMETRIC_VIRTUAL_ENABLED = "biometric_virtual_enabled";
 
         /**
+         * Whether or not fingerprint virtual sensors are enabled.
+         * @hide
+         */
+        @FlaggedApi("com.android.server.biometrics.face_vhal_feature")
+        public static final String BIOMETRIC_FINGERPRINT_VIRTUAL_ENABLED =
+                "biometric_fingerprint_virtual_enabled";
+
+        /**
+         * Whether or not face virtual sensors are enabled.
+         * @hide
+         */
+        @FlaggedApi("com.android.server.biometrics.face_vhal_feature")
+        public static final String BIOMETRIC_FACE_VIRTUAL_ENABLED =
+                "biometric_face_virtual_enabled";
+
+        /**
          * Whether or not biometric is allowed on Keyguard.
          * @hide
          */
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 3662d69..d2a533c 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -124,10 +124,13 @@
     public static final int CUJ_BACK_PANEL_ARROW = 88;
     public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK = 89;
     public static final int CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90;
+    public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91;
+    public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92;
+    public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = 93;
 
     // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
     @VisibleForTesting
-    static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+    static final int LAST_CUJ = CUJ_LAUNCHER_SAVE_APP_PAIR;
 
     /** @hide */
     @IntDef({
@@ -212,6 +215,9 @@
             CUJ_BACK_PANEL_ARROW,
             CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK,
             CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH,
+            CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+            CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR,
+            CUJ_LAUNCHER_SAVE_APP_PAIR
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -306,6 +312,9 @@
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_BACK;
         CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_WEB_SEARCH;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SAVE_APP_PAIR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SAVE_APP_PAIR;
     }
 
     private Cuj() {
@@ -484,6 +493,12 @@
                 return "LAUNCHER_CLOSE_ALL_APPS_BACK";
             case CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH:
                 return "LAUNCHER_SEARCH_QSB_WEB_SEARCH";
+            case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE:
+                return "LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE";
+            case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR:
+                return "LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR";
+            case CUJ_LAUNCHER_SAVE_APP_PAIR:
+                return "LAUNCHER_SAVE_APP_PAIR";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 0ec8b74..a288fb7 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -165,6 +165,9 @@
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK;
     @Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME;
+    @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
+    @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
+    @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR;
 
     private static class InstanceHolder {
         public static final InteractionJankMonitor INSTANCE =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index c26604a..7c2ba45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -293,7 +293,13 @@
     @Override
     public void onFoldStateChanged(boolean isFolded) {
         if (isFolded) {
+            // Reset unfold animation finished flag on folding, so it could be used next time
+            // when we unfold the device as an indication that animation hasn't finished yet
             mAnimationFinished = false;
+
+            // If we are currently animating unfold animation we should finish it because
+            // the animation might not start and finish as the device was folded
+            finishTransitionIfNeeded();
         }
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
index c5e229f..acc0bce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
@@ -298,6 +298,32 @@
     }
 
     @Test
+    public void fold_animationInProgress_finishesTransition() {
+        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
+        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
+
+        // Unfold
+        mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ false);
+        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
+        mUnfoldTransitionHandler.startAnimation(
+                mTransition,
+                mock(TransitionInfo.class),
+                mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class),
+                finishCallback
+        );
+
+        // Start animation but don't finish it
+        mShellUnfoldProgressProvider.onStateChangeStarted();
+        mShellUnfoldProgressProvider.onStateChangeProgress(0.5f);
+
+        // Fold
+        mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ true);
+
+        verify(finishCallback).onTransitionFinished(any());
+    }
+
+    @Test
     public void mergeAnimation_eatsDisplayOnlyTransitions() {
         TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
         mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index 221ca4f..4763c0b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -244,8 +244,7 @@
     }
 
     private static boolean isArchivingEnabled() {
-        return android.content.pm.Flags.archiving()
-                || SystemProperties.getBoolean("pm.archiving.enabled", false);
+        return android.content.pm.Flags.archiving();
     }
 
     private boolean isCloneProfile(UserHandle userHandle) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index a395266..e1e1ee5 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -151,7 +151,7 @@
     }
 
     private fun isArchivingEnabled(featureFlags: FeatureFlags) =
-            featureFlags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false)
+            featureFlags.archiving()
 
     override fun showSystemPredicate(
         userIdFlow: Flow<Int>,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 28cdc6d..4f9e11a 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -848,6 +848,8 @@
                         Settings.Secure.BIOMETRIC_APP_ENABLED,
                         Settings.Secure.BIOMETRIC_KEYGUARD_ENABLED,
                         Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED,
+                        Settings.Secure.BIOMETRIC_FINGERPRINT_VIRTUAL_ENABLED,
+                        Settings.Secure.BIOMETRIC_FACE_VIRTUAL_ENABLED,
                         Settings.Secure.BLUETOOTH_ADDR_VALID,
                         Settings.Secure.BLUETOOTH_ADDRESS,
                         Settings.Secure.BLUETOOTH_NAME,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index cc2e84c..1129c63 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -326,6 +326,7 @@
         "androidx.test.uiautomator_uiautomator",
         "androidx.core_core-animation-testing",
         "mockito-target-extended-minus-junit4",
+        "mockito-kotlin2",
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
         "kotlin-test",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index dd1fc23..8e73379 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -343,6 +343,16 @@
 }
 
 flag {
+   name: "activity_transition_use_largest_window"
+   namespace: "systemui"
+   description: "Target largest opening window during activity transitions."
+   bug: "323294573"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
    name: "centralized_status_bar_height_fix"
    namespace: "systemui"
    description: "Refactors shade header and keyguard status bar to read status bar dimens from a"
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 99b7c36..2268d16 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -44,6 +44,7 @@
         "androidx.core_core-animation-nodeps",
         "androidx.core_core-ktx",
         "androidx.annotation_annotation",
+        "com_android_systemui_flags_lib",
         "SystemUIShaderLib",
         "WindowManager-Shell-shared",
         "animationlib",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 1b99e19..ea1cb34 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -43,6 +43,7 @@
 import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.systemui.Flags.activityTransitionUseLargestWindow
 import kotlin.math.roundToInt
 
 private const val TAG = "ActivityTransitionAnimator"
@@ -648,11 +649,27 @@
             var candidate: RemoteAnimationTarget? = null
             for (it in apps) {
                 if (it.mode == RemoteAnimationTarget.MODE_OPENING) {
-                    if (!it.hasAnimatingParent) {
-                        return it
-                    }
-                    if (candidate == null) {
-                        candidate = it
+                    if (activityTransitionUseLargestWindow()) {
+                        if (
+                            candidate == null ||
+                                !it.hasAnimatingParent && candidate.hasAnimatingParent
+                        ) {
+                            candidate = it
+                            continue
+                        }
+                        if (
+                            !it.hasAnimatingParent &&
+                                it.screenSpaceBounds.hasGreaterAreaThan(candidate.screenSpaceBounds)
+                        ) {
+                            candidate = it
+                        }
+                    } else {
+                        if (!it.hasAnimatingParent) {
+                            return it
+                        }
+                        if (candidate == null) {
+                            candidate = it
+                        }
                     }
                 }
             }
@@ -960,5 +977,9 @@
                 e.printStackTrace()
             }
         }
+
+        private fun Rect.hasGreaterAreaThan(other: Rect): Boolean {
+            return (this.width() * this.height()) > (other.width() * other.height())
+        }
     }
 }
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
index c988b4a..eeb64bd8 100644
--- a/packages/SystemUI/res/layout/screenshot_shelf.xml
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -51,15 +51,7 @@
         <LinearLayout
             android:id="@+id/screenshot_actions"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content">
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/screenshot_share_chip"/>
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/screenshot_edit_chip"/>
-            <include layout="@layout/overlay_action_chip"
-                     android:id="@+id/screenshot_scroll_chip"
-                     android:visibility="gone" />
-        </LinearLayout>
+            android:layout_height="wrap_content" />
     </HorizontalScrollView>
     <View
         android:id="@+id/screenshot_preview_border"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
new file mode 100644
index 0000000..caa67df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.app.ActivityOptions
+import android.app.BroadcastOptions
+import android.app.ExitTransitionCoordinator
+import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks
+import android.app.PendingIntent
+import android.content.Intent
+import android.os.UserHandle
+import android.util.Log
+import android.util.Pair
+import android.view.View
+import android.view.Window
+import com.android.app.tracing.coroutines.launch
+import com.android.internal.app.ChooserActivity
+import com.android.systemui.dagger.qualifiers.Application
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+
+class ActionExecutor
+@AssistedInject
+constructor(
+    private val intentExecutor: ActionIntentExecutor,
+    @Application private val applicationScope: CoroutineScope,
+    @Assisted val window: Window,
+    @Assisted val transitionView: View,
+    @Assisted val onDismiss: (() -> Unit)
+) {
+
+    var isPendingSharedTransition = false
+        private set
+
+    fun startSharedTransition(intent: Intent, user: UserHandle, overrideTransition: Boolean) {
+        isPendingSharedTransition = true
+        val windowTransition = createWindowTransition()
+        applicationScope.launch("$TAG#launchIntentAsync") {
+            intentExecutor.launchIntent(
+                intent,
+                user,
+                overrideTransition,
+                windowTransition.first,
+                windowTransition.second
+            )
+        }
+    }
+
+    fun sendPendingIntent(pendingIntent: PendingIntent) {
+        try {
+            val options = BroadcastOptions.makeBasic()
+            options.setInteractive(true)
+            options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            )
+            pendingIntent.send(options.toBundle())
+            onDismiss.invoke()
+        } catch (e: PendingIntent.CanceledException) {
+            Log.e(TAG, "Intent cancelled", e)
+        }
+    }
+
+    /**
+     * Supplies the necessary bits for the shared element transition to share sheet. Note that once
+     * called, the action intent to share must be sent immediately after.
+     */
+    private fun createWindowTransition(): Pair<ActivityOptions, ExitTransitionCoordinator> {
+        val callbacks: ExitTransitionCallbacks =
+            object : ExitTransitionCallbacks {
+                override fun isReturnTransitionAllowed(): Boolean {
+                    return false
+                }
+
+                override fun hideSharedElements() {
+                    isPendingSharedTransition = false
+                    onDismiss.invoke()
+                }
+
+                override fun onFinish() {}
+            }
+        return ActivityOptions.startSharedElementAnimation(
+            window,
+            callbacks,
+            null,
+            Pair.create(transitionView, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+        )
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(window: Window, transitionView: View, onDismiss: (() -> Unit)): ActionExecutor
+    }
+
+    companion object {
+        private const val TAG = "ActionExecutor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
index 8e9769ab..a0cef52 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt
@@ -23,7 +23,9 @@
 import android.content.Context
 import android.content.Intent
 import android.net.Uri
+import android.os.UserHandle
 import com.android.systemui.res.R
+import com.android.systemui.screenshot.scroll.LongScreenshotActivity
 
 object ActionIntentCreator {
     /** @return a chooser intent to share the given URI. */
@@ -89,6 +91,14 @@
             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
     }
 
+    /** @return an Intent to start the LongScreenshotActivity */
+    fun createLongScreenshotIntent(owner: UserHandle, context: Context): Intent {
+        return Intent(context, LongScreenshotActivity::class.java)
+            .putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, owner)
+            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+    }
+
     private const val EXTRA_EDIT_SOURCE = "edit_source"
     private const val EDIT_SOURCE_SCREENSHOT = "screenshot"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 1f9853b..4eca51d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -25,7 +25,6 @@
 import android.os.RemoteException
 import android.os.UserHandle
 import android.util.Log
-import android.util.Pair
 import android.view.IRemoteAnimationFinishedCallback
 import android.view.IRemoteAnimationRunner
 import android.view.RemoteAnimationAdapter
@@ -67,20 +66,22 @@
      */
     fun launchIntentAsync(
         intent: Intent,
-        transition: Pair<ActivityOptions, ExitTransitionCoordinator>?,
         user: UserHandle,
         overrideTransition: Boolean,
+        options: ActivityOptions?,
+        transitionCoordinator: ExitTransitionCoordinator?,
     ) {
         applicationScope.launch("$TAG#launchIntentAsync") {
-            launchIntent(intent, transition, user, overrideTransition)
+            launchIntent(intent, user, overrideTransition, options, transitionCoordinator)
         }
     }
 
     suspend fun launchIntent(
         intent: Intent,
-        transition: Pair<ActivityOptions, ExitTransitionCoordinator>?,
         user: UserHandle,
         overrideTransition: Boolean,
+        options: ActivityOptions?,
+        transitionCoordinator: ExitTransitionCoordinator?,
     ) {
         if (screenshotActionDismissSystemWindows()) {
             keyguardController.dismiss()
@@ -90,14 +91,12 @@
         } else {
             dismissKeyguard()
         }
-        transition?.second?.startExit()
+        transitionCoordinator?.startExit()
 
         if (user == myUserHandle()) {
-            withContext(mainDispatcher) {
-                context.startActivity(intent, transition?.first?.toBundle())
-            }
+            withContext(mainDispatcher) { context.startActivity(intent, options?.toBundle()) }
         } else {
-            launchCrossProfileIntent(user, intent, transition?.first?.toBundle())
+            launchCrossProfileIntent(user, intent, options?.toBundle())
         }
 
         if (overrideTransition) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
index a1481f6..4cf18fb 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
@@ -34,9 +34,9 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.res.R
-import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -113,7 +113,7 @@
     override fun setChipIntents(imageData: ScreenshotController.SavedImageData) =
         view.setChipIntents(imageData)
 
-    override fun requestDismissal(event: ScreenshotEvent) {
+    override fun requestDismissal(event: ScreenshotEvent?) {
         if (DEBUG_DISMISS) {
             Log.d(TAG, "screenshot dismissal requested")
         }
@@ -124,7 +124,7 @@
             }
             return
         }
-        logger.log(event, 0, packageName)
+        event?.let { logger.log(event, 0, packageName) }
         view.animateDismissal()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index bbf7ed5..4914409 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -165,6 +165,7 @@
                     mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
                     mParams.owner);
             mImageData.subject = getSubjectString(mImageTime);
+            mImageData.imageTime = mImageTime;
 
             mParams.mActionsReadyListener.onActionsReady(mImageData);
             if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
index 97acccd..f69021f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -16,78 +16,180 @@
 
 package com.android.systemui.screenshot
 
+import android.app.assist.AssistContent
 import android.content.Context
-import android.content.Intent
-import android.graphics.drawable.Drawable
-import android.net.Uri
-import android.os.UserHandle
+import android.util.Log
 import androidx.appcompat.content.res.AppCompatResources
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.log.DebugLogger.debugLog
 import com.android.systemui.res.R
-import javax.inject.Inject
+import com.android.systemui.screenshot.ActionIntentCreator.createEdit
+import com.android.systemui.screenshot.ActionIntentCreator.createShareWithSubject
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 
 /**
  * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI
  * implementation.
  */
 interface ScreenshotActionsProvider {
-    data class ScreenshotAction(
-        val icon: Drawable? = null,
-        val text: String? = null,
-        val description: String,
-        val overrideTransition: Boolean = false,
-        val retrieveIntent: (Uri) -> Intent
-    )
+    fun onScrollChipReady(onClick: Runnable)
+    fun setCompletedScreenshot(result: ScreenshotSavedResult)
 
-    interface ScreenshotActionsCallback {
-        fun setPreviewAction(overrideTransition: Boolean = false, retrieveIntent: (Uri) -> Intent)
-        fun addAction(action: ScreenshotAction) = addActions(listOf(action))
-        fun addActions(actions: List<ScreenshotAction>)
-    }
+    fun onAssistContentAvailable(assistContent: AssistContent) {}
 
     interface Factory {
         fun create(
-            context: Context,
-            user: UserHandle?,
-            callback: ScreenshotActionsCallback
+            request: ScreenshotData,
+            requestId: String,
+            actionExecutor: ActionExecutor,
         ): ScreenshotActionsProvider
     }
 }
 
-class DefaultScreenshotActionsProvider(
+class DefaultScreenshotActionsProvider
+@AssistedInject
+constructor(
     private val context: Context,
-    private val user: UserHandle?,
-    private val callback: ScreenshotActionsProvider.ScreenshotActionsCallback
+    private val viewModel: ScreenshotViewModel,
+    private val smartActionsProvider: SmartActionsProvider,
+    private val uiEventLogger: UiEventLogger,
+    @Assisted val request: ScreenshotData,
+    @Assisted val requestId: String,
+    @Assisted val actionExecutor: ActionExecutor,
 ) : ScreenshotActionsProvider {
+    private var pendingAction: ((ScreenshotSavedResult) -> Unit)? = null
+    private var result: ScreenshotSavedResult? = null
+
     init {
-        callback.setPreviewAction(true) { ActionIntentCreator.createEdit(it, context) }
-        val editAction =
-            ScreenshotActionsProvider.ScreenshotAction(
+        viewModel.setPreviewAction {
+            debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" }
+            uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString)
+            onDeferrableActionTapped { result ->
+                actionExecutor.startSharedTransition(
+                    createEdit(result.uri, context),
+                    result.user,
+                    true
+                )
+            }
+        }
+        viewModel.addAction(
+            ActionButtonViewModel(
                 AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
                 context.resources.getString(R.string.screenshot_edit_label),
                 context.resources.getString(R.string.screenshot_edit_description),
-                true
-            ) { uri ->
-                ActionIntentCreator.createEdit(uri, context)
+            ) {
+                debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
+                uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
+                onDeferrableActionTapped { result ->
+                    actionExecutor.startSharedTransition(
+                        createEdit(result.uri, context),
+                        result.user,
+                        true
+                    )
+                }
             }
-        val shareAction =
-            ScreenshotActionsProvider.ScreenshotAction(
+        )
+        viewModel.addAction(
+            ActionButtonViewModel(
                 AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
                 context.resources.getString(R.string.screenshot_share_label),
                 context.resources.getString(R.string.screenshot_share_description),
-                false
-            ) { uri ->
-                ActionIntentCreator.createShare(uri)
+            ) {
+                debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" }
+                uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString)
+                onDeferrableActionTapped { result ->
+                    actionExecutor.startSharedTransition(
+                        createShareWithSubject(result.uri, result.subject),
+                        result.user,
+                        false
+                    )
+                }
             }
-        callback.addActions(listOf(editAction, shareAction))
+        )
+        smartActionsProvider.requestQuickShare(request, requestId) { quickShare ->
+            if (!quickShare.actionIntent.isImmutable) {
+                viewModel.addAction(
+                    ActionButtonViewModel(
+                        quickShare.getIcon().loadDrawable(context),
+                        quickShare.title,
+                        quickShare.title
+                    ) {
+                        debugLog(LogConfig.DEBUG_ACTIONS) { "Quickshare tapped" }
+                        onDeferrableActionTapped { result ->
+                            uiEventLogger.log(
+                                SCREENSHOT_SMART_ACTION_TAPPED,
+                                0,
+                                request.packageNameString
+                            )
+                            val pendingIntentWithUri =
+                                smartActionsProvider.wrapIntent(
+                                    quickShare,
+                                    result.uri,
+                                    result.subject,
+                                    requestId
+                                )
+                            actionExecutor.sendPendingIntent(pendingIntentWithUri)
+                        }
+                    }
+                )
+            } else {
+                Log.w(TAG, "Received immutable quick share pending intent; ignoring")
+            }
+        }
     }
 
-    class Factory @Inject constructor() : ScreenshotActionsProvider.Factory {
-        override fun create(
-            context: Context,
-            user: UserHandle?,
-            callback: ScreenshotActionsProvider.ScreenshotActionsCallback
-        ): ScreenshotActionsProvider {
-            return DefaultScreenshotActionsProvider(context, user, callback)
+    override fun onScrollChipReady(onClick: Runnable) {
+        viewModel.addAction(
+            ActionButtonViewModel(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_scroll),
+                context.resources.getString(R.string.screenshot_scroll_label),
+                context.resources.getString(R.string.screenshot_scroll_label),
+            ) {
+                onClick.run()
+            }
+        )
+    }
+
+    override fun setCompletedScreenshot(result: ScreenshotSavedResult) {
+        if (this.result != null) {
+            Log.e(TAG, "Got a second completed screenshot for existing request!")
+            return
         }
+        this.result = result
+        pendingAction?.invoke(result)
+        smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions ->
+            viewModel.addActions(
+                smartActions.map {
+                    ActionButtonViewModel(it.getIcon().loadDrawable(context), it.title, it.title) {
+                        actionExecutor.sendPendingIntent(it.actionIntent)
+                    }
+                }
+            )
+        }
+    }
+
+    private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) {
+        result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult }
+    }
+
+    @AssistedFactory
+    interface Factory : ScreenshotActionsProvider.Factory {
+        override fun create(
+            request: ScreenshotData,
+            requestId: String,
+            actionExecutor: ActionExecutor,
+        ): DefaultScreenshotActionsProvider
+    }
+
+    companion object {
+        private const val TAG = "ScreenshotActionsProvider"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 597e773..f43882c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -29,14 +29,11 @@
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT;
 
-import static java.util.Objects.requireNonNull;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ExitTransitionCoordinator;
 import android.app.ICompatCameraControlCallback;
@@ -53,7 +50,6 @@
 import android.hardware.display.DisplayManager;
 import android.net.Uri;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -61,18 +57,12 @@
 import android.util.Log;
 import android.util.Pair;
 import android.view.Display;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationTarget;
 import android.view.ScrollCaptureResponse;
 import android.view.View;
 import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
-import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 import android.window.WindowContext;
 
@@ -80,16 +70,14 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.PhoneWindow;
 import com.android.settingslib.applications.InterestingConfigChanges;
+import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.res.R;
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
-import com.android.systemui.screenshot.scroll.LongScreenshotActivity;
-import com.android.systemui.screenshot.scroll.LongScreenshotData;
-import com.android.systemui.screenshot.scroll.ScrollCaptureClient;
-import com.android.systemui.screenshot.scroll.ScrollCaptureController;
+import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor;
 import com.android.systemui.util.Assert;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -98,13 +86,13 @@
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
+import kotlin.Unit;
+
 import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
+import java.util.UUID;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 import java.util.function.Consumer;
 
 import javax.inject.Provider;
@@ -116,34 +104,6 @@
 public class ScreenshotController {
     private static final String TAG = logTag(ScreenshotController.class);
 
-    private ScrollCaptureResponse mLastScrollCaptureResponse;
-    private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest;
-
-    /**
-     * This is effectively a no-op, but we need something non-null to pass in, in order to
-     * successfully override the pending activity entrance animation.
-     */
-    static final IRemoteAnimationRunner.Stub SCREENSHOT_REMOTE_RUNNER =
-            new IRemoteAnimationRunner.Stub() {
-                @Override
-                public void onAnimationStart(
-                        @WindowManager.TransitionOldType int transit,
-                        RemoteAnimationTarget[] apps,
-                        RemoteAnimationTarget[] wallpapers,
-                        RemoteAnimationTarget[] nonApps,
-                        final IRemoteAnimationFinishedCallback finishedCallback) {
-                    try {
-                        finishedCallback.onAnimationFinished();
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "Error finishing screenshot remote animation", e);
-                    }
-                }
-
-                @Override
-                public void onAnimationCancelled() {
-                }
-            };
-
     /**
      * POD used in the AsyncTask which saves an image in the background.
      */
@@ -169,6 +129,7 @@
         public Notification.Action quickShareAction;
         public UserHandle owner;
         public String subject;  // Title for sharing
+        public Long imageTime; // Time at which screenshot was saved
 
         /**
          * Used to reset the return data on error
@@ -178,6 +139,7 @@
             smartActions = null;
             quickShareAction = null;
             subject = null;
+            imageTime = null;
         }
     }
 
@@ -216,17 +178,10 @@
     // ScreenshotNotificationSmartActionsProvider.
     static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
     static final String EXTRA_ID = "android:screenshot_id";
-    static final String ACTION_TYPE_DELETE = "Delete";
-    static final String ACTION_TYPE_SHARE = "Share";
-    static final String ACTION_TYPE_EDIT = "Edit";
     static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
-    static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition";
     static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
     static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin";
 
-    static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
-    static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
-    static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
 
     // From WizardManagerHelper.java
     private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
@@ -245,23 +200,21 @@
     private final Executor mMainExecutor;
     private final ExecutorService mBgExecutor;
     private final BroadcastSender mBroadcastSender;
+    private final BroadcastDispatcher mBroadcastDispatcher;
+    private final ActionExecutor mActionExecutor;
 
     private final WindowManager mWindowManager;
     private final WindowManager.LayoutParams mWindowLayoutParams;
-    private final AccessibilityManager mAccessibilityManager;
     @Nullable
     private final ScreenshotSoundController mScreenshotSoundController;
-    private final ScrollCaptureClient mScrollCaptureClient;
     private final PhoneWindow mWindow;
     private final DisplayManager mDisplayManager;
     private final int mDisplayId;
-    private final ScrollCaptureController mScrollCaptureController;
-    private final LongScreenshotData mLongScreenshotHolder;
-    private final boolean mIsLowRamDevice;
+    private final ScrollCaptureExecutor mScrollCaptureExecutor;
     private final ScreenshotNotificationSmartActionsProvider
             mScreenshotNotificationSmartActionsProvider;
     private final TimeoutHandler mScreenshotHandler;
-    private final ActionIntentExecutor mActionExecutor;
+    private final ActionIntentExecutor mActionIntentExecutor;
     private final UserManager mUserManager;
     private final AssistContentRequester mAssistContentRequester;
 
@@ -270,13 +223,11 @@
     private SaveImageInBackgroundTask mSaveInBgTask;
     private boolean mScreenshotTakenInPortrait;
     private boolean mBlockAttach;
-
-    private ScreenshotActionsProvider mActionsProvider;
-
     private Animator mScreenshotAnimation;
     private RequestCallback mCurrentRequestCallback;
+    private ScreenshotActionsProvider mActionsProvider;
     private String mPackageName = "";
-    private BroadcastReceiver mCopyBroadcastReceiver;
+    private final BroadcastReceiver mCopyBroadcastReceiver;
 
     // When false, the screenshot is taken without showing the ui. Note that this only applies to
     // external displays, as on the default one the UI should **always** be shown.
@@ -298,23 +249,24 @@
     @AssistedInject
     ScreenshotController(
             Context context,
+            DisplayManager displayManager,
+            WindowManager windowManager,
             FeatureFlags flags,
             ScreenshotViewProxy.Factory viewProxyFactory,
             ScreenshotActionsProvider.Factory actionsProviderFactory,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
-            ScrollCaptureClient scrollCaptureClient,
             UiEventLogger uiEventLogger,
             ImageExporter imageExporter,
             ImageCapture imageCapture,
             @Main Executor mainExecutor,
-            ScrollCaptureController scrollCaptureController,
-            LongScreenshotData longScreenshotHolder,
-            ActivityManager activityManager,
+            ScrollCaptureExecutor scrollCaptureExecutor,
             TimeoutHandler timeoutHandler,
             BroadcastSender broadcastSender,
+            BroadcastDispatcher broadcastDispatcher,
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
-            ActionIntentExecutor actionExecutor,
+            ActionIntentExecutor actionIntentExecutor,
+            ActionExecutor.Factory actionExecutorFactory,
             UserManager userManager,
             AssistContentRequester assistContentRequester,
             MessageContainerController messageContainerController,
@@ -323,36 +275,34 @@
             @Assisted boolean showUIOnExternalDisplay
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
+        mActionsProviderFactory = actionsProviderFactory;
         mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
-        mScrollCaptureClient = scrollCaptureClient;
         mUiEventLogger = uiEventLogger;
         mImageExporter = imageExporter;
         mImageCapture = imageCapture;
         mMainExecutor = mainExecutor;
-        mScrollCaptureController = scrollCaptureController;
-        mLongScreenshotHolder = longScreenshotHolder;
-        mIsLowRamDevice = activityManager.isLowRamDevice();
+        mScrollCaptureExecutor = scrollCaptureExecutor;
         mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider;
         mBgExecutor = Executors.newSingleThreadExecutor();
         mBroadcastSender = broadcastSender;
+        mBroadcastDispatcher = broadcastDispatcher;
 
         mScreenshotHandler = timeoutHandler;
         mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
 
 
         mDisplayId = displayId;
-        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+        mDisplayManager = displayManager;
+        mWindowManager = windowManager;
         final Context displayContext = context.createDisplayContext(getDisplay());
         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
-        mWindowManager = mContext.getSystemService(WindowManager.class);
         mFlags = flags;
-        mActionExecutor = actionExecutor;
+        mActionIntentExecutor = actionIntentExecutor;
         mUserManager = userManager;
         mMessageContainerController = messageContainerController;
         mAssistContentRequester = assistContentRequester;
 
         mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId);
-        mActionsProviderFactory = actionsProviderFactory;
 
         mScreenshotHandler.setOnTimeoutRunnable(() -> {
             if (DEBUG_UI) {
@@ -361,8 +311,6 @@
             mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT);
         });
 
-        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
-
         // Setup the window that we are going to use
         mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
         mWindowLayoutParams.setTitle("ScreenshotAnimation");
@@ -373,6 +321,12 @@
         mConfigChanges.applyNewConfig(context.getResources());
         reloadAssets();
 
+        mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy.getScreenshotPreview(),
+                () -> {
+                    requestDismissal(null);
+                    return Unit.INSTANCE;
+                });
+
         // Sound is only reproduced from the controller of the default display.
         if (displayId == Display.DEFAULT_DISPLAY) {
             mScreenshotSoundController = screenshotSoundController.get();
@@ -388,9 +342,9 @@
                 }
             }
         };
-        mContext.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
-                        ClipboardOverlayController.COPY_OVERLAY_ACTION),
-                ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED);
+        mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
+                        ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null,
+                Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION);
         mShowUIOnExternalDisplay = showUIOnExternalDisplay;
     }
 
@@ -448,8 +402,23 @@
             return;
         }
 
-        saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
-                this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+        if (screenshotShelfUi()) {
+            final UUID requestId = UUID.randomUUID();
+            final String screenshotId = String.format("Screenshot_%s", requestId);
+            mActionsProvider = mActionsProviderFactory.create(
+                    screenshot, screenshotId, mActionExecutor);
+            saveScreenshotInBackground(screenshot, requestId, finisher);
+
+            if (screenshot.getTaskId() >= 0) {
+                mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
+                        assistContent -> {
+                            mActionsProvider.onAssistContentAvailable(assistContent);
+                        });
+            }
+        } else {
+            saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
+                    this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+        }
 
         // The window is focusable by default
         setWindowFocusable(true);
@@ -484,7 +453,9 @@
         // ignore system bar insets for the purpose of window layout
         mWindow.getDecorView().setOnApplyWindowInsetsListener(
                 (v, insets) -> WindowInsets.CONSUMED);
-        mScreenshotHandler.cancelTimeout(); // restarted after animation
+        if (!screenshotShelfUi()) {
+            mScreenshotHandler.cancelTimeout(); // restarted after animation
+        }
     }
 
     private boolean shouldShowUi() {
@@ -504,11 +475,6 @@
 
         mViewProxy.reset();
 
-        if (screenshotShelfUi()) {
-            mActionsProvider = mActionsProviderFactory.create(mContext, screenshot.getUserHandle(),
-                    ((ScreenshotActionsProvider.ScreenshotActionsCallback) mViewProxy));
-        }
-
         if (mViewProxy.isAttachedToWindow()) {
             // if we didn't already dismiss for another reason
             if (!mViewProxy.isDismissing()) {
@@ -536,7 +502,11 @@
     }
 
     boolean isPendingSharedTransition() {
-        return mViewProxy.isPendingSharedTransition();
+        if (screenshotShelfUi()) {
+            return mActionExecutor.isPendingSharedTransition();
+        } else {
+            return mViewProxy.isPendingSharedTransition();
+        }
     }
 
     // Any cleanup needed when the service is being destroyed.
@@ -555,7 +525,7 @@
      * Release the constructed window context.
      */
     private void releaseContext() {
-        mContext.unregisterReceiver(mCopyBroadcastReceiver);
+        mBroadcastDispatcher.unregisterReceiver(mCopyBroadcastReceiver);
         mContext.release();
     }
 
@@ -584,8 +554,9 @@
 
             @Override
             public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) {
-                mActionExecutor.launchIntentAsync(
-                        intent, createWindowTransition(), owner, overrideTransition);
+                Pair<ActivityOptions, ExitTransitionCoordinator> exit = createWindowTransition();
+                mActionIntentExecutor.launchIntentAsync(
+                        intent, owner, overrideTransition, exit.first, exit.second);
             }
 
             @Override
@@ -603,7 +574,7 @@
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mViewProxy.getView());
         }
-        setContentView(mViewProxy.getView());
+        mWindow.setContentView(mViewProxy.getView());
     }
 
     private void enqueueScrollCaptureRequest(UserHandle owner) {
@@ -622,9 +593,8 @@
                                 mViewProxy.hideScrollChip();
                                 // Delay scroll capture eval a bit to allow the underlying activity
                                 // to set up in the new orientation.
-                                mScreenshotHandler.postDelayed(() -> {
-                                    requestScrollCapture(owner);
-                                }, 150);
+                                mScreenshotHandler.postDelayed(
+                                        () -> requestScrollCapture(owner), 150);
                                 mViewProxy.updateInsets(
                                         mWindowManager.getCurrentWindowMetrics().getWindowInsets());
                                 // Screenshot animation calculations won't be valid anymore,
@@ -647,120 +617,51 @@
     }
 
     private void requestScrollCapture(UserHandle owner) {
-        if (!allowLongScreenshots()) {
-            Log.d(TAG, "Long screenshots not supported on this device");
+        mScrollCaptureExecutor.requestScrollCapture(
+                mDisplayId,
+                mWindow.getDecorView().getWindowToken(),
+                (response) -> {
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
+                            0, response.getPackageName());
+                    if (screenshotShelfUi() && mActionsProvider != null) {
+                        mActionsProvider.onScrollChipReady(
+                                () -> onScrollButtonClicked(owner, response));
+                    } else {
+                        mViewProxy.showScrollChip(response.getPackageName(),
+                                () -> onScrollButtonClicked(owner, response));
+                    }
+                    return Unit.INSTANCE;
+                }
+        );
+    }
+
+    private void onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response) {
+        if (DEBUG_INPUT) {
+            Log.d(TAG, "scroll chip tapped");
+        }
+        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
+                response.getPackageName());
+        Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, getFullScreenRect());
+        if (newScreenshot == null) {
+            Log.e(TAG, "Failed to capture current screenshot for scroll transition!");
             return;
         }
-        mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken());
-        if (mLastScrollCaptureRequest != null) {
-            mLastScrollCaptureRequest.cancel(true);
-        }
-        final ListenableFuture<ScrollCaptureResponse> future = mScrollCaptureClient.request(
-                mDisplayId);
-        mLastScrollCaptureRequest = future;
-        mLastScrollCaptureRequest.addListener(() ->
-                onScrollCaptureResponseReady(future, owner), mMainExecutor);
+        // delay starting scroll capture to make sure scrim is up before the app moves
+        mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
+                mScreenshotTakenInPortrait, () -> executeBatchScrollCapture(response, owner));
     }
 
-    private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture,
-            UserHandle owner) {
-        try {
-            if (mLastScrollCaptureResponse != null) {
-                mLastScrollCaptureResponse.close();
-                mLastScrollCaptureResponse = null;
-            }
-            if (responseFuture.isCancelled()) {
-                return;
-            }
-            mLastScrollCaptureResponse = responseFuture.get();
-            if (!mLastScrollCaptureResponse.isConnected()) {
-                // No connection means that the target window wasn't found
-                // or that it cannot support scroll capture.
-                Log.d(TAG, "ScrollCapture: " + mLastScrollCaptureResponse.getDescription() + " ["
-                        + mLastScrollCaptureResponse.getWindowTitle() + "]");
-                return;
-            }
-            Log.d(TAG, "ScrollCapture: connected to window ["
-                    + mLastScrollCaptureResponse.getWindowTitle() + "]");
+    private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
+        mScrollCaptureExecutor.executeBatchScrollCapture(response,
+                () -> {
+                    final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent(
+                            owner, mContext);
+                    mActionIntentExecutor.launchIntentAsync(intent, owner, true,
+                            ActivityOptions.makeCustomAnimation(mContext, 0, 0), null);
 
-            final ScrollCaptureResponse response = mLastScrollCaptureResponse;
-            mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> {
-                DisplayMetrics displayMetrics = new DisplayMetrics();
-                getDisplay().getRealMetrics(displayMetrics);
-                Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
-                        new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
-
-                if (newScreenshot != null) {
-                    // delay starting scroll capture to make sure scrim is up before the app moves
-                    mViewProxy.prepareScrollingTransition(
-                            response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait,
-                            () -> runBatchScrollCapture(response, owner));
-                } else {
-                    Log.wtf(TAG, "failed to capture current screenshot for scroll transition");
-                }
-            });
-        } catch (InterruptedException | ExecutionException e) {
-            Log.e(TAG, "requestScrollCapture failed", e);
-        }
-    }
-
-    ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture;
-
-    private void runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
-        // Clear the reference to prevent close() in dismissScreenshot
-        mLastScrollCaptureResponse = null;
-
-        if (mLongScreenshotFuture != null) {
-            mLongScreenshotFuture.cancel(true);
-        }
-        mLongScreenshotFuture = mScrollCaptureController.run(response);
-        mLongScreenshotFuture.addListener(() -> {
-            ScrollCaptureController.LongScreenshot longScreenshot;
-            try {
-                longScreenshot = mLongScreenshotFuture.get();
-            } catch (CancellationException e) {
-                Log.e(TAG, "Long screenshot cancelled");
-                return;
-            } catch (InterruptedException | ExecutionException e) {
-                Log.e(TAG, "Exception", e);
-                mViewProxy.restoreNonScrollingUi();
-                return;
-            }
-
-            if (longScreenshot.getHeight() == 0) {
-                mViewProxy.restoreNonScrollingUi();
-                return;
-            }
-
-            mLongScreenshotHolder.setLongScreenshot(longScreenshot);
-            mLongScreenshotHolder.setTransitionDestinationCallback(
-                    (transitionDestination, onTransitionEnd) -> {
-                        mViewProxy.startLongScreenshotTransition(
-                                transitionDestination, onTransitionEnd,
-                                longScreenshot);
-                        // TODO: Do this via ActionIntentExecutor instead.
-                        mContext.closeSystemDialogs();
-                    }
-            );
-
-            final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
-            intent.putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE,
-                    owner);
-            intent.setFlags(
-                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-            mContext.startActivity(intent,
-                    ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle());
-            RemoteAnimationAdapter runner = new RemoteAnimationAdapter(
-                    SCREENSHOT_REMOTE_RUNNER, 0, 0);
-            try {
-                WindowManagerGlobal.getWindowManagerService()
-                        .overridePendingAppTransitionRemote(runner,
-                                mDisplayId);
-            } catch (Exception e) {
-                Log.e(TAG, "Error overriding screenshot app transition", e);
-            }
-        }, mMainExecutor);
+                },
+                mViewProxy::restoreNonScrollingUi,
+                mViewProxy::startLongScreenshotTransition);
     }
 
     private void withWindowAttached(Runnable action) {
@@ -785,10 +686,6 @@
         }
     }
 
-    private void setContentView(View contentView) {
-        mWindow.setContentView(contentView);
-    }
-
     @MainThread
     private void attachWindow() {
         View decorView = mWindow.getDecorView();
@@ -900,28 +797,16 @@
                     public void onFinish() {
                     }
                 };
-        Pair<ActivityOptions, ExitTransitionCoordinator> transition =
-                ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
-                        Pair.create(mViewProxy.getScreenshotPreview(),
-                                ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
 
-        return transition;
+        return ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
+                Pair.create(mViewProxy.getScreenshotPreview(),
+                        ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
     }
 
     /** Reset screenshot view and then call onCompleteRunnable */
     private void finishDismiss() {
         Log.d(TAG, "finishDismiss");
-        if (mLastScrollCaptureRequest != null) {
-            mLastScrollCaptureRequest.cancel(true);
-            mLastScrollCaptureRequest = null;
-        }
-        if (mLastScrollCaptureResponse != null) {
-            mLastScrollCaptureResponse.close();
-            mLastScrollCaptureResponse = null;
-        }
-        if (mLongScreenshotFuture != null) {
-            mLongScreenshotFuture.cancel(true);
-        }
+        mScrollCaptureExecutor.close();
         if (mCurrentRequestCallback != null) {
             mCurrentRequestCallback.onFinish();
             mCurrentRequestCallback = null;
@@ -931,6 +816,35 @@
         mScreenshotHandler.cancelTimeout();
     }
 
+    private void saveScreenshotInBackground(
+            ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
+        ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
+                requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplayId);
+        future.addListener(() -> {
+            try {
+                ImageExporter.Result result = future.get();
+                Log.d(TAG, "Saved screenshot: " + result);
+                logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
+                mScreenshotHandler.resetTimeout();
+                if (result.uri != null) {
+                    mActionsProvider.setCompletedScreenshot(new ScreenshotSavedResult(
+                            result.uri, screenshot.getUserOrDefault(), result.timestamp));
+                }
+                if (DEBUG_CALLBACK) {
+                    Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
+                            + "finisher.accept(\"" + result.uri + "\"");
+                }
+                finisher.accept(result.uri);
+            } catch (Exception e) {
+                Log.d(TAG, "Failed to store screenshot", e);
+                if (DEBUG_CALLBACK) {
+                    Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
+                }
+                finisher.accept(null);
+            }
+        }, mMainExecutor);
+    }
+
     /**
      * Creates a new worker thread and saves the screenshot to the media store.
      */
@@ -966,13 +880,12 @@
      */
     private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) {
         logSuccessOnActionsReady(imageData);
-        if (DEBUG_UI) {
-            Log.d(TAG, "Showing UI actions");
-        }
-
         mScreenshotHandler.resetTimeout();
 
         if (imageData.uri != null) {
+            if (DEBUG_UI) {
+                Log.d(TAG, "Showing UI actions");
+            }
             if (!imageData.owner.equals(Process.myUserHandle())) {
                 Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as "
                         + imageData.uri);
@@ -1020,20 +933,27 @@
     /**
      * Logs success/failure of the screenshot saving task, and shows an error if it failed.
      */
-    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
-        if (imageData.uri == null) {
+    private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
+        if (uri == null) {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
             mNotificationsController.notifyScreenshotError(
                     R.string.screenshot_failed_to_save_text);
         } else {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
-            if (mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
+            if (mUserManager.isManagedProfile(owner.getIdentifier())) {
                 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0,
                         mPackageName);
             }
         }
     }
 
+    /**
+     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
+     */
+    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
+        logScreenshotResultStatus(imageData.uri, imageData.owner);
+    }
+
     private boolean isUserSetupComplete(UserHandle owner) {
         return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
                 .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
@@ -1070,10 +990,6 @@
         return mDisplayManager.getDisplay(mDisplayId);
     }
 
-    private boolean allowLongScreenshots() {
-        return !mIsLowRamDevice;
-    }
-
     private Rect getFullScreenRect() {
         DisplayMetrics displayMetrics = new DisplayMetrics();
         getDisplay().getRealMetrics(displayMetrics);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index 92e933a..4fdd90b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -5,6 +5,7 @@
 import android.graphics.Insets
 import android.graphics.Rect
 import android.net.Uri
+import android.os.Process
 import android.os.UserHandle
 import android.view.Display
 import android.view.WindowManager.ScreenshotSource
@@ -31,6 +32,10 @@
     val packageNameString: String
         get() = if (topComponent == null) "" else topComponent!!.packageName
 
+    fun getUserOrDefault(): UserHandle {
+        return userHandle ?: Process.myUserHandle()
+    }
+
     companion object {
         @JvmStatic
         fun fromRequest(request: ScreenshotRequest, displayId: Int = Display.DEFAULT_DISPLAY) =
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt
new file mode 100644
index 0000000..5b6e7ac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.net.Uri
+import android.os.UserHandle
+import java.text.DateFormat
+import java.util.Date
+
+/**
+ * Represents a saved screenshot, with the uri and user it was saved to as well as the time it was
+ * saved.
+ */
+data class ScreenshotSavedResult(val uri: Uri, val user: UserHandle, val imageTime: Long) {
+    val subject: String
+
+    init {
+        val subjectDate = DateFormat.getDateTimeInstance().format(Date(imageTime))
+        subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate)
+    }
+
+    companion object {
+        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 88bca95..6b9332b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -20,10 +20,8 @@
 import android.animation.AnimatorListenerAdapter
 import android.app.Notification
 import android.content.Context
-import android.content.Intent
 import android.graphics.Bitmap
 import android.graphics.Rect
-import android.net.Uri
 import android.view.KeyEvent
 import android.view.LayoutInflater
 import android.view.ScrollCaptureResponse
@@ -35,7 +33,6 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.log.DebugLogger.debugLog
 import com.android.systemui.res.R
-import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
 import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
 import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
 import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
@@ -45,7 +42,6 @@
 import com.android.systemui.screenshot.ui.ScreenshotAnimationController
 import com.android.systemui.screenshot.ui.ScreenshotShelfView
 import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
-import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -59,7 +55,7 @@
     private val viewModel: ScreenshotViewModel,
     @Assisted private val context: Context,
     @Assisted private val displayId: Int
-) : ScreenshotViewProxy, ScreenshotActionsProvider.ScreenshotActionsCallback {
+) : ScreenshotViewProxy {
     override val view: ScreenshotShelfView =
         LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
     override val screenshotPreview: View
@@ -77,8 +73,6 @@
     override var isPendingSharedTransition = false
 
     private val animationController = ScreenshotAnimationController(view)
-    private var imageData: SavedImageData? = null
-    private var runOnImageDataAcquired: ((SavedImageData) -> Unit)? = null
 
     init {
         ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
@@ -91,9 +85,7 @@
     override fun reset() {
         animationController.cancel()
         isPendingSharedTransition = false
-        imageData = null
         viewModel.reset()
-        runOnImageDataAcquired = null
     }
     override fun updateInsets(insets: WindowInsets) {}
     override fun updateOrientation(insets: WindowInsets) {}
@@ -104,12 +96,9 @@
 
     override fun addQuickShareChip(quickShareAction: Notification.Action) {}
 
-    override fun setChipIntents(data: SavedImageData) {
-        imageData = data
-        runOnImageDataAcquired?.invoke(data)
-    }
+    override fun setChipIntents(imageData: SavedImageData) {}
 
-    override fun requestDismissal(event: ScreenshotEvent) {
+    override fun requestDismissal(event: ScreenshotEvent?) {
         debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
 
         // If we're already animating out, don't restart the animation
@@ -117,7 +106,7 @@
             debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
             return
         }
-        logger.log(event, 0, packageName)
+        event?.let { logger.log(it, 0, packageName) }
         val animator = animationController.getExitAnimation()
         animator.addListener(
             object : AnimatorListenerAdapter() {
@@ -143,13 +132,18 @@
         newScreenshot: Bitmap,
         screenshotTakenInPortrait: Boolean,
         onTransitionPrepared: Runnable,
-    ) {}
+    ) {
+        onTransitionPrepared.run()
+    }
 
     override fun startLongScreenshotTransition(
         transitionDestination: Rect,
         onTransitionEnd: Runnable,
         longScreenshot: ScrollCaptureController.LongScreenshot
-    ) {}
+    ) {
+        onTransitionEnd.run()
+        callbacks?.onDismiss()
+    }
 
     override fun restoreNonScrollingUi() {}
 
@@ -219,41 +213,4 @@
     interface Factory : ScreenshotViewProxy.Factory {
         override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
     }
-
-    override fun setPreviewAction(overrideTransition: Boolean, retrieveIntent: (Uri) -> Intent) {
-        viewModel.setPreviewAction {
-            imageData?.let {
-                val intent = retrieveIntent(it.uri)
-                debugLog(DEBUG_ACTIONS) { "Preview tapped: $intent" }
-                isPendingSharedTransition = true
-                callbacks?.onAction(intent, it.owner, overrideTransition)
-            }
-        }
-    }
-
-    override fun addActions(actions: List<ScreenshotActionsProvider.ScreenshotAction>) {
-        viewModel.addActions(
-            actions.map { action ->
-                ActionButtonViewModel(action.icon, action.text, action.description) {
-                    val actionRunnable =
-                        getActionRunnable(action.retrieveIntent, action.overrideTransition)
-                    imageData?.let { actionRunnable(it) }
-                        ?: run { runOnImageDataAcquired = actionRunnable }
-                }
-            }
-        )
-    }
-
-    private fun getActionRunnable(
-        retrieveIntent: (Uri) -> Intent,
-        overrideTransition: Boolean
-    ): (SavedImageData) -> Unit {
-        val onClick: (SavedImageData) -> Unit = {
-            val intent = retrieveIntent(it.uri)
-            debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" }
-            isPendingSharedTransition = true
-            callbacks!!.onAction(intent, it.owner, overrideTransition)
-        }
-        return onClick
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 65e8457..59e38a8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -259,16 +259,8 @@
         if (DEBUG_SCROLL) {
             Log.d(TAG, "Showing Scroll option");
         }
-        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, packageName);
         mScrollChip.setVisibility(VISIBLE);
-        mScrollChip.setOnClickListener((v) -> {
-            if (DEBUG_INPUT) {
-                Log.d(TAG, "scroll chip tapped");
-            }
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
-                    packageName);
-            onClick.run();
-        });
+        mScrollChip.setOnClickListener((v) -> onClick.run());
     }
 
     @Override // ViewTreeObserver.OnComputeInternalInsetsListener
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
index 6be32a9..a4069d1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
@@ -46,7 +46,7 @@
     fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator
     fun addQuickShareChip(quickShareAction: Notification.Action)
     fun setChipIntents(imageData: ScreenshotController.SavedImageData)
-    fun requestDismissal(event: ScreenshotEvent)
+    fun requestDismissal(event: ScreenshotEvent?)
 
     fun showScrollChip(packageName: String, onClick: Runnable)
     fun hideScrollChip()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
new file mode 100644
index 0000000..a895b30
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2024 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.app.Notification
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.os.Process
+import android.os.SystemClock
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION
+import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import javax.inject.Inject
+import kotlin.random.Random
+
+/**
+ * Handle requesting smart/quickshare actions from the provider and executing an action when the
+ * action futures complete.
+ */
+class SmartActionsProvider
+@Inject
+constructor(
+    private val context: Context,
+    private val smartActions: ScreenshotNotificationSmartActionsProvider,
+) {
+    /**
+     * Requests quick share action for a given screenshot.
+     *
+     * @param data the ScreenshotData request
+     * @param id the request id for the screenshot
+     * @param onAction callback to run when quick share action is returned
+     */
+    fun requestQuickShare(
+        data: ScreenshotData,
+        id: String,
+        onAction: (Notification.Action) -> Unit
+    ) {
+        val bitmap = data.bitmap ?: return
+        val component = data.topComponent ?: ComponentName("", "")
+        requestQuickShareAction(id, bitmap, component, data.getUserOrDefault()) { quickShare ->
+            onAction(quickShare)
+        }
+    }
+
+    /**
+     * Requests smart actions for a given screenshot.
+     *
+     * @param data the ScreenshotData request
+     * @param id the request id for the screenshot
+     * @param result the data for the saved image
+     * @param onActions callback to run when actions are returned
+     */
+    fun requestSmartActions(
+        data: ScreenshotData,
+        id: String,
+        result: ScreenshotSavedResult,
+        onActions: (List<Notification.Action>) -> Unit
+    ) {
+        val bitmap = data.bitmap ?: return
+        val component = data.topComponent ?: ComponentName("", "")
+        requestSmartActions(
+            id,
+            bitmap,
+            component,
+            data.getUserOrDefault(),
+            result.uri,
+            REGULAR_SMART_ACTIONS
+        ) { actions ->
+            onActions(actions)
+        }
+    }
+
+    /**
+     * Wraps the given quick share action in a broadcast intent.
+     *
+     * @param quickShare the quick share action to wrap
+     * @param uri the URI of the saved screenshot
+     * @param subject the subject/title for the screenshot
+     * @param id the request ID of the screenshot
+     * @return the pending intent with correct URI
+     */
+    fun wrapIntent(
+        quickShare: Notification.Action,
+        uri: Uri,
+        subject: String,
+        id: String
+    ): PendingIntent {
+        val wrappedIntent: Intent =
+            Intent(context, SmartActionsReceiver::class.java)
+                .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
+                .putExtra(
+                    ScreenshotController.EXTRA_ACTION_INTENT_FILLIN,
+                    createFillInIntent(uri, subject)
+                )
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+        val extras: Bundle = quickShare.extras
+        val actionType =
+            extras.getString(
+                ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
+                ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE
+            )
+        // We only query for quick share actions when smart actions are enabled, so we can assert
+        // that it's true here.
+        wrappedIntent
+            .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType)
+            .putExtra(ScreenshotController.EXTRA_ID, id)
+            .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, true)
+        return PendingIntent.getBroadcast(
+            context,
+            Random.nextInt(),
+            wrappedIntent,
+            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+
+    private fun createFillInIntent(uri: Uri, subject: String): Intent {
+        val fillIn = Intent()
+        fillIn.setType("image/png")
+        fillIn.putExtra(Intent.EXTRA_STREAM, uri)
+        fillIn.putExtra(Intent.EXTRA_SUBJECT, subject)
+        // Include URI in ClipData also, so that grantPermission picks it up.
+        // We don't use setData here because some apps interpret this as "to:".
+        val clipData =
+            ClipData(ClipDescription("content", arrayOf("image/png")), ClipData.Item(uri))
+        fillIn.clipData = clipData
+        fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+        return fillIn
+    }
+
+    private fun requestQuickShareAction(
+        id: String,
+        image: Bitmap,
+        component: ComponentName,
+        user: UserHandle,
+        timeoutMs: Long = 500,
+        onAction: (Notification.Action) -> Unit
+    ) {
+        requestSmartActions(id, image, component, user, null, QUICK_SHARE_ACTION, timeoutMs) {
+            it.firstOrNull()?.let { action -> onAction(action) }
+        }
+    }
+
+    private fun requestSmartActions(
+        id: String,
+        image: Bitmap,
+        component: ComponentName,
+        user: UserHandle,
+        uri: Uri?,
+        actionType: ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType,
+        timeoutMs: Long = 500,
+        onActions: (List<Notification.Action>) -> Unit
+    ) {
+        val enabled = isSmartActionsEnabled(user)
+        debugLog(DEBUG_ACTIONS) {
+            ("getSmartActionsFuture id=$id, uri=$uri, provider=$smartActions, " +
+                "actionType=$actionType, smartActionsEnabled=$enabled, userHandle=$user")
+        }
+        if (!enabled) {
+            debugLog(DEBUG_ACTIONS) { "Screenshot Intelligence not enabled, returning empty list" }
+            onActions(listOf())
+            return
+        }
+        if (image.config != Bitmap.Config.HARDWARE) {
+            debugLog(DEBUG_ACTIONS) {
+                "Bitmap expected: Hardware, Bitmap found: ${image.config}. Returning empty list."
+            }
+            onActions(listOf())
+            return
+        }
+        val smartActionsFuture: CompletableFuture<List<Notification.Action>>
+        val startTimeMs = SystemClock.uptimeMillis()
+        try {
+            smartActionsFuture =
+                smartActions.getActions(id, uri, image, component, actionType, user)
+        } catch (e: Throwable) {
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS, error = e) {
+                "Failed to get future for screenshot notification smart actions."
+            }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.REQUEST_SMART_ACTIONS,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR,
+                waitTimeMs
+            )
+            onActions(listOf())
+            return
+        }
+        try {
+            val actions = smartActionsFuture.get(timeoutMs, TimeUnit.MILLISECONDS)
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS) {
+                ("Got ${actions.size} smart actions. Wait time: $waitTimeMs ms, " +
+                    "actionType=$actionType")
+            }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
+                waitTimeMs
+            )
+            onActions(actions)
+        } catch (e: Throwable) {
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS, error = e) {
+                "Error getting smart actions. Wait time: $waitTimeMs ms, actionType=$actionType"
+            }
+            val status =
+                if (e is TimeoutException) {
+                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
+                } else {
+                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR
+                }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
+                status,
+                waitTimeMs
+            )
+            onActions(listOf())
+        }
+    }
+
+    private fun notifyScreenshotOp(
+        screenshotId: String,
+        op: ScreenshotNotificationSmartActionsProvider.ScreenshotOp,
+        status: ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus,
+        durationMs: Long
+    ) {
+        debugLog(DEBUG_ACTIONS) {
+            "$smartActions notifyOp: $op id=$screenshotId, status=$status, durationMs=$durationMs"
+        }
+        try {
+            smartActions.notifyOp(screenshotId, op, status, durationMs)
+        } catch (e: Throwable) {
+            Log.e(TAG, "Error in notifyScreenshotOp: ", e)
+        }
+    }
+
+    private fun isSmartActionsEnabled(user: UserHandle): Boolean {
+        // Smart actions don't yet work for cross-user saves.
+        val savingToOtherUser = user !== Process.myUserHandle()
+        val actionsEnabled =
+            DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
+                true
+            )
+        return !savingToOtherUser && actionsEnabled
+    }
+
+    companion object {
+        private const val TAG = "SmartActionsProvider"
+        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
index 1e1a577..706ac9c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java
@@ -335,8 +335,8 @@
             // TODO: Fix transition for work profile. Omitting it in the meantime.
             mActionExecutor.launchIntentAsync(
                     ActionIntentCreator.INSTANCE.createEdit(uri, this),
-                    null,
-                    mScreenshotUserHandle, false);
+                    mScreenshotUserHandle, false,
+                    /* activityOptions */ null, /* transitionCoordinator */ null);
         } else {
             String editorPackage = getString(R.string.config_screenshotEditor);
             Intent intent = new Intent(Intent.ACTION_EDIT);
@@ -363,7 +363,8 @@
 
     private void doShare(Uri uri) {
         Intent shareIntent = ActionIntentCreator.INSTANCE.createShare(uri);
-        mActionExecutor.launchIntentAsync(shareIntent, null, mScreenshotUserHandle, false);
+        mActionExecutor.launchIntentAsync(shareIntent, mScreenshotUserHandle, false,
+                /* activityOptions */ null, /* transitionCoordinator */ null);
     }
 
     private void onClicked(View v) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt
new file mode 100644
index 0000000..6c4ee3e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 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.scroll
+
+import android.app.ActivityManager
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.Log
+import android.view.ScrollCaptureResponse
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.screenshot.scroll.ScrollCaptureController.LongScreenshot
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executor
+import java.util.concurrent.Future
+import javax.inject.Inject
+
+class ScrollCaptureExecutor
+@Inject
+constructor(
+    activityManager: ActivityManager,
+    private val scrollCaptureClient: ScrollCaptureClient,
+    private val scrollCaptureController: ScrollCaptureController,
+    private val longScreenshotHolder: LongScreenshotData,
+    @Main private val mainExecutor: Executor
+) {
+    private val isLowRamDevice = activityManager.isLowRamDevice
+    private var lastScrollCaptureRequest: ListenableFuture<ScrollCaptureResponse>? = null
+    private var lastScrollCaptureResponse: ScrollCaptureResponse? = null
+    private var longScreenshotFuture: ListenableFuture<LongScreenshot>? = null
+
+    fun requestScrollCapture(
+        displayId: Int,
+        token: IBinder,
+        callback: (ScrollCaptureResponse) -> Unit
+    ) {
+        if (!allowLongScreenshots()) {
+            Log.d(TAG, "Long screenshots not supported on this device")
+            return
+        }
+        scrollCaptureClient.setHostWindowToken(token)
+        lastScrollCaptureRequest?.cancel(true)
+        val scrollRequest =
+            scrollCaptureClient.request(displayId).apply {
+                addListener(
+                    { onScrollCaptureResponseReady(this)?.let { callback.invoke(it) } },
+                    mainExecutor
+                )
+            }
+        lastScrollCaptureRequest = scrollRequest
+    }
+
+    fun interface ScrollTransitionReady {
+        fun onTransitionReady(
+            destRect: Rect,
+            onTransitionEnd: Runnable,
+            longScreenshot: LongScreenshot
+        )
+    }
+
+    fun executeBatchScrollCapture(
+        response: ScrollCaptureResponse,
+        onCaptureComplete: Runnable,
+        onFailure: Runnable,
+        transition: ScrollTransitionReady,
+    ) {
+        // Clear the reference to prevent close() on reset
+        lastScrollCaptureResponse = null
+        longScreenshotFuture?.cancel(true)
+        longScreenshotFuture =
+            scrollCaptureController.run(response).apply {
+                addListener(
+                    {
+                        getLongScreenshotChecked(this, onFailure)?.let {
+                            longScreenshotHolder.setLongScreenshot(it)
+                            longScreenshotHolder.setTransitionDestinationCallback {
+                                destinationRect: Rect,
+                                onTransitionEnd: Runnable ->
+                                transition.onTransitionReady(destinationRect, onTransitionEnd, it)
+                            }
+                            onCaptureComplete.run()
+                        }
+                    },
+                    mainExecutor
+                )
+            }
+    }
+
+    fun close() {
+        lastScrollCaptureRequest?.cancel(true)
+        lastScrollCaptureRequest = null
+        lastScrollCaptureResponse?.close()
+        lastScrollCaptureResponse = null
+        longScreenshotFuture?.cancel(true)
+    }
+
+    private fun getLongScreenshotChecked(
+        future: ListenableFuture<LongScreenshot>,
+        onFailure: Runnable
+    ): LongScreenshot? {
+        var longScreenshot: LongScreenshot? = null
+        runCatching { longScreenshot = future.get() }
+            .onFailure {
+                Log.e(TAG, "Caught exception", it)
+                onFailure.run()
+                return null
+            }
+        if (longScreenshot?.height != 0) {
+            return longScreenshot
+        }
+        onFailure.run()
+        return null
+    }
+
+    private fun onScrollCaptureResponseReady(
+        responseFuture: Future<ScrollCaptureResponse>
+    ): ScrollCaptureResponse? {
+        try {
+            lastScrollCaptureResponse?.close()
+            lastScrollCaptureResponse = null
+            if (responseFuture.isCancelled) {
+                return null
+            }
+            val captureResponse = responseFuture.get().apply { lastScrollCaptureResponse = this }
+            if (!captureResponse.isConnected) {
+                // No connection means that the target window wasn't found
+                // or that it cannot support scroll capture.
+                Log.d(
+                    TAG,
+                    "ScrollCapture: ${captureResponse.description} [${captureResponse.windowTitle}]"
+                )
+                return null
+            }
+            Log.d(TAG, "ScrollCapture: connected to window [${captureResponse.windowTitle}]")
+            return captureResponse
+        } catch (e: InterruptedException) {
+            Log.e(TAG, "requestScrollCapture interrupted", e)
+        } catch (e: ExecutionException) {
+            Log.e(TAG, "requestScrollCapture failed", e)
+        }
+        return null
+    }
+
+    private fun allowLongScreenshots(): Boolean {
+        return !isLowRamDevice
+    }
+
+    private companion object {
+        private const val TAG = "ScrollCaptureExecutor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
index c7fe3f6..a6374ae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -36,6 +36,7 @@
         } else {
             view.setOnClickListener(null)
         }
+        view.tag = viewModel.id
         view.contentDescription = viewModel.description
         view.visibility = View.VISIBLE
         view.alpha = 1f
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
index d878200..32e9296 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -60,7 +60,7 @@
                     }
                     launch {
                         viewModel.previewAction.collect { onClick ->
-                            previewView.setOnClickListener { onClick?.run() }
+                            previewView.setOnClickListener { onClick?.invoke() }
                         }
                     }
                     launch {
@@ -70,21 +70,36 @@
                                     .requireViewById<View>(R.id.actions_container_background)
                                     .visibility = View.VISIBLE
                             }
-                            val viewPool = actionsContainer.children.toList()
-                            actionsContainer.removeAllViews()
-                            val actionButtons =
-                                List(actions.size) {
-                                    viewPool.getOrElse(it) {
+
+                            // Remove any buttons not in the new list, then do another pass to add
+                            // any new actions and update any that are already there.
+                            // This assumes that actions can never change order and that each action
+                            // ID is unique.
+                            val newIds = actions.map { it.id }
+
+                            for (view in actionsContainer.children.toList()) {
+                                if (view.tag !in newIds) {
+                                    actionsContainer.removeView(view)
+                                }
+                            }
+
+                            for ((index, action) in actions.withIndex()) {
+                                val currentView: View? = actionsContainer.getChildAt(index)
+                                if (action.id == currentView?.tag) {
+                                    // Same ID, update the display
+                                    ActionButtonViewBinder.bind(currentView, action)
+                                } else {
+                                    // Different ID. Removals have already happened so this must
+                                    // mean that the new action must be inserted here.
+                                    val actionButton =
                                         layoutInflater.inflate(
                                             R.layout.overlay_action_chip,
                                             actionsContainer,
                                             false
                                         )
-                                    }
+                                    actionsContainer.addView(actionButton, index)
+                                    ActionButtonViewBinder.bind(actionButton, action)
                                 }
-                            actionButtons.zip(actions).forEach {
-                                actionsContainer.addView(it.first)
-                                ActionButtonViewBinder.bind(it.first, it.second)
                             }
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
index 05bfed1..97b24c1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -22,5 +22,13 @@
     val icon: Drawable?,
     val name: CharSequence?,
     val description: CharSequence,
-    val onClicked: (() -> Unit)?
-)
+    val onClicked: (() -> Unit)?,
+) {
+    val id: Int = getId()
+
+    companion object {
+        private var nextId = 0
+
+        private fun getId() = nextId.also { nextId += 1 }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
index dc61d1e..ddfa69b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -24,8 +24,8 @@
 class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
     private val _preview = MutableStateFlow<Bitmap?>(null)
     val preview: StateFlow<Bitmap?> = _preview
-    private val _previewAction = MutableStateFlow<Runnable?>(null)
-    val previewAction: StateFlow<Runnable?> = _previewAction
+    private val _previewAction = MutableStateFlow<(() -> Unit)?>(null)
+    val previewAction: StateFlow<(() -> Unit)?> = _previewAction
     private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
     val actions: StateFlow<List<ActionButtonViewModel>> = _actions
     val showDismissButton: Boolean
@@ -35,8 +35,14 @@
         _preview.value = bitmap
     }
 
-    fun setPreviewAction(runnable: Runnable) {
-        _previewAction.value = runnable
+    fun setPreviewAction(onClick: () -> Unit) {
+        _previewAction.value = onClick
+    }
+
+    fun addAction(action: ActionButtonViewModel) {
+        val actionList = _actions.value.toMutableList()
+        actionList.add(action)
+        _actions.value = actionList
     }
 
     fun addActions(actions: List<ActionButtonViewModel>) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
new file mode 100644
index 0000000..5e7d8fb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 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.app.PendingIntent
+import android.content.Intent
+import android.os.Bundle
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.Window
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyBlocking
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ActionExecutorTest : SysuiTestCase() {
+    private val scheduler = TestCoroutineScheduler()
+    private val mainDispatcher = StandardTestDispatcher(scheduler)
+    private val testScope = TestScope(mainDispatcher)
+
+    private val intentExecutor = mock<ActionIntentExecutor>()
+    private val window = mock<Window>()
+    private val view = mock<View>()
+    private val onDismiss = mock<(() -> Unit)>()
+    private val pendingIntent = mock<PendingIntent>()
+
+    private lateinit var actionExecutor: ActionExecutor
+
+    @Test
+    fun startSharedTransition_callsLaunchIntent() = runTest {
+        actionExecutor = createActionExecutor()
+
+        actionExecutor.startSharedTransition(Intent(Intent.ACTION_EDIT), UserHandle.CURRENT, true)
+        scheduler.advanceUntilIdle()
+
+        val intentCaptor = argumentCaptor<Intent>()
+        verifyBlocking(intentExecutor) {
+            launchIntent(capture(intentCaptor), eq(UserHandle.CURRENT), eq(true), any(), any())
+        }
+        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT)
+    }
+
+    @Test
+    fun sendPendingIntent_dismisses() = runTest {
+        actionExecutor = createActionExecutor()
+
+        actionExecutor.sendPendingIntent(pendingIntent)
+
+        verify(pendingIntent).send(any(Bundle::class.java))
+        verify(onDismiss).invoke()
+    }
+
+    private fun createActionExecutor(): ActionExecutor {
+        return ActionExecutor(intentExecutor, testScope, window, view, onDismiss)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt
index 0c32470..5e53fe1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt
@@ -64,7 +64,7 @@
             val intent = Intent(Intent.ACTION_EDIT).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK }
             val userHandle = myUserHandle()
 
-            actionIntentExecutor.launchIntent(intent, null, userHandle, false)
+            actionIntentExecutor.launchIntent(intent, userHandle, false, null, null)
             scheduler.advanceUntilIdle()
 
             verify(activityManagerWrapper)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
new file mode 100644
index 0000000..bde821b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 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.app.Notification
+import android.app.PendingIntent
+import android.content.Intent
+import android.net.Uri
+import android.os.Process
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import android.view.accessibility.AccessibilityManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.clipboardoverlay.EditTextActivity
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class DefaultScreenshotActionsProviderTest : SysuiTestCase() {
+    private val actionExecutor = mock<ActionExecutor>()
+    private val accessibilityManager = mock<AccessibilityManager>()
+    private val uiEventLogger = mock<UiEventLogger>()
+    private val smartActionsProvider = mock<SmartActionsProvider>()
+
+    private val request = ScreenshotData.forTesting()
+    private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0)
+
+    private lateinit var viewModel: ScreenshotViewModel
+    private lateinit var actionsProvider: ScreenshotActionsProvider
+
+    @Before
+    fun setUp() {
+        viewModel = ScreenshotViewModel(accessibilityManager)
+        request.userHandle = UserHandle.OWNER
+    }
+
+    @Test
+    fun previewActionAccessed_beforeScreenshotCompleted_doesNothing() {
+        actionsProvider = createActionsProvider()
+
+        assertNotNull(viewModel.previewAction.value)
+        viewModel.previewAction.value!!.invoke()
+        verifyNoMoreInteractions(actionExecutor)
+    }
+
+    @Test
+    fun actionButtonsAccessed_beforeScreenshotCompleted_doesNothing() {
+        actionsProvider = createActionsProvider()
+
+        assertThat(viewModel.actions.value.size).isEqualTo(2)
+        val firstAction = viewModel.actions.value[0]
+        assertThat(firstAction.onClicked).isNotNull()
+        val secondAction = viewModel.actions.value[1]
+        assertThat(secondAction.onClicked).isNotNull()
+        firstAction.onClicked!!.invoke()
+        secondAction.onClicked!!.invoke()
+        verifyNoMoreInteractions(actionExecutor)
+    }
+
+    @Test
+    fun actionAccessed_withResult_launchesIntent() = runTest {
+        actionsProvider = createActionsProvider()
+
+        actionsProvider.setCompletedScreenshot(validResult)
+        viewModel.actions.value[0].onClicked!!.invoke()
+
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED), eq(0), eq(""))
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(actionExecutor)
+            .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(true))
+        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT)
+    }
+
+    @Test
+    fun actionAccessed_whilePending_launchesMostRecentAction() = runTest {
+        actionsProvider = createActionsProvider()
+
+        viewModel.actions.value[0].onClicked!!.invoke()
+        viewModel.previewAction.value!!.invoke()
+        viewModel.actions.value[1].onClicked!!.invoke()
+        actionsProvider.setCompletedScreenshot(validResult)
+
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED), eq(0), eq(""))
+        val intentCaptor = argumentCaptor<Intent>()
+        verify(actionExecutor)
+            .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(false))
+        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CHOOSER)
+    }
+
+    @Test
+    fun quickShareTapped_wrapsAndSendsIntent() = runTest {
+        val quickShare =
+            Notification.Action(
+                R.drawable.ic_screenshot_edit,
+                "TestQuickShare",
+                PendingIntent.getActivity(
+                    context,
+                    0,
+                    Intent(context, EditTextActivity::class.java),
+                    PendingIntent.FLAG_MUTABLE
+                )
+            )
+        whenever(smartActionsProvider.requestQuickShare(any(), any(), any())).then {
+            (it.getArgument(2) as ((Notification.Action) -> Unit)).invoke(quickShare)
+        }
+        whenever(smartActionsProvider.wrapIntent(any(), any(), any(), any())).thenAnswer {
+            (it.getArgument(0) as Notification.Action).actionIntent
+        }
+        actionsProvider = createActionsProvider()
+
+        viewModel.actions.value[2].onClicked?.invoke()
+        verify(uiEventLogger, never())
+            .log(eq(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED), any(), any())
+        verify(smartActionsProvider, never()).wrapIntent(any(), any(), any(), any())
+        actionsProvider.setCompletedScreenshot(validResult)
+        verify(smartActionsProvider)
+            .wrapIntent(eq(quickShare), eq(validResult.uri), eq(validResult.subject), eq("testid"))
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED), eq(0), eq(""))
+    }
+
+    private fun createActionsProvider(): ScreenshotActionsProvider {
+        return DefaultScreenshotActionsProvider(
+            context,
+            viewModel,
+            smartActionsProvider,
+            uiEventLogger,
+            request,
+            "testid",
+            actionExecutor
+        )
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index f51b62d..4af30a9 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -89,11 +89,23 @@
         return true;
     }
 
-    /** If virtualized biometrics are supported (requires debug build). */
-    public static boolean isVirtualEnabled(@NonNull Context context) {
+    /** If virtualized fingerprint sensor is supported. */
+    public static boolean isFingerprintVirtualEnabled(@NonNull Context context) {
         return Build.isDebuggable()
-                && Settings.Secure.getIntForUser(context.getContentResolver(),
-                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 0, UserHandle.USER_CURRENT) == 1;
+                && (Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.BIOMETRIC_FINGERPRINT_VIRTUAL_ENABLED, 0,
+                UserHandle.USER_CURRENT) == 1
+                || Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 0, UserHandle.USER_CURRENT) == 1);
+    }
+
+    /** If virtualized face sensor is supported. */
+    public static boolean isFaceVirtualEnabled(@NonNull Context context) {
+        return Build.isDebuggable()
+                && (Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.BIOMETRIC_FACE_VIRTUAL_ENABLED, 0, UserHandle.USER_CURRENT) == 1
+                || Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 0, UserHandle.USER_CURRENT) == 1);
     }
 
     /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 7ee2a7a..f4c34ca 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -748,7 +748,7 @@
             final Pair<String, SensorProps[]> virtualSensorProps = faceSensorConfigurations
                     .getSensorPairForInstance("virtual");
 
-            if (Utils.isVirtualEnabled(getContext())) {
+            if (Utils.isFaceVirtualEnabled(getContext())) {
                 if (virtualSensorProps != null) {
                     return virtualSensorProps;
                 } else {
@@ -778,7 +778,7 @@
             }
 
             final int virtualAt = aidlInstances.indexOf("virtual");
-            if (Flags.faceVhalFeature() && Utils.isVirtualEnabled(getContext())) {
+            if (Flags.faceVhalFeature() && Utils.isFaceVirtualEnabled(getContext())) {
                 if (virtualAt != -1) {
                     //only virtual instance should be returned
                     Slog.i(TAG, "virtual hal is used");
@@ -920,7 +920,7 @@
 
     void syncEnrollmentsNow() {
         Utils.checkPermissionOrShell(getContext(), MANAGE_FACE);
-        if (Flags.faceVhalFeature() && Utils.isVirtualEnabled(getContext())) {
+        if (Flags.faceVhalFeature() && Utils.isFaceVirtualEnabled(getContext())) {
             Slog.i(TAG, "Sync virtual enrollments");
             final int userId = ActivityManager.getCurrentUser();
             for (ServiceProvider provider : mRegistry.getProviders()) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 1ba1213..9785691 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -1136,7 +1136,7 @@
 
         final Pair<String, SensorProps[]> virtualSensorPropsPair = fingerprintSensorConfigurations
                 .getSensorPairForInstance("virtual");
-        if (Utils.isVirtualEnabled(getContext())) {
+        if (Utils.isFingerprintVirtualEnabled(getContext())) {
             if (virtualSensorPropsPair != null) {
                 return virtualSensorPropsPair;
             } else {
@@ -1160,7 +1160,7 @@
         }
 
         final int virtualAt = aidlInstances.indexOf("virtual");
-        if (Utils.isVirtualEnabled(getContext())) {
+        if (Utils.isFingerprintVirtualEnabled(getContext())) {
             if (virtualAt != -1) {
                 //only virtual instance should be returned
                 Slog.i(TAG, "virtual hal is used");
@@ -1286,7 +1286,7 @@
 
     void syncEnrollmentsNow() {
         Utils.checkPermissionOrShell(getContext(), MANAGE_FINGERPRINT);
-        if (Utils.isVirtualEnabled(getContext())) {
+        if (Utils.isFingerprintVirtualEnabled(getContext())) {
             Slog.i(TAG, "Sync virtual enrollments");
             final int userId = ActivityManager.getCurrentUser();
             final CountDownLatch latch = new CountDownLatch(mRegistry.getProviders().size());
@@ -1315,7 +1315,7 @@
     }
 
     void simulateVhalFingerDown() {
-        if (Utils.isVirtualEnabled(getContext())) {
+        if (Utils.isFingerprintVirtualEnabled(getContext())) {
             Slog.i(TAG, "Simulate virtual HAL finger down event");
             final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider != null) {
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index a12d248..b24caf4 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -222,7 +222,7 @@
         } else {
             // As external display is enabled by default, need to disable it now.
             // TODO(b/292196201) Remove when the display can be disabled before DPC is created.
-            logicalDisplay.setEnabledLocked(false);
+            mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, false);
         }
 
         if (!isExternalDisplayAllowed()) {
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 372e18b..7fc72ae 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -186,7 +186,7 @@
     }
 
     public static boolean isArchivingEnabled() {
-        return Flags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false);
+        return Flags.archiving();
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 3f2e12b..a6954ee 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1922,6 +1922,7 @@
             accessibilityManager.performSystemAction(
                     AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
         }
+        dismissKeyboardShortcutsMenu();
     }
 
     private void toggleNotificationPanel() {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 149f55f..271c74e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8519,7 +8519,9 @@
             }
         // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
         // are already calculated in resolveFixedOrientationConfiguration.
-        } else if (!isLetterboxedForFixedOrientationAndAspectRatio()) {
+        // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer.
+        } else if (!isLetterboxedForFixedOrientationAndAspectRatio()
+                && !mLetterboxUiController.hasFullscreenOverride()) {
             resolveAspectRatioRestriction(newParentConfiguration);
         }
 
@@ -8620,7 +8622,7 @@
 
         // Override starts here.
         final Rect stableInsets = mDisplayContent.getDisplayPolicy().getDecorInsetsInfo(
-                rotation, fullBounds.width(), fullBounds.height()).mLegacyConfigInsets;
+                rotation, fullBounds.width(), fullBounds.height()).mOverrideConfigInsets;
         // This should be the only place override the configuration for ActivityRecord. Override
         // the value if not calculated yet.
         Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds();
@@ -8656,7 +8658,7 @@
             mDisplayContent.getDisplay().getDisplayInfo(info);
             mDisplayContent.computeSizeRanges(info, rotated, info.logicalWidth,
                     info.logicalHeight, mDisplayContent.getDisplayMetrics().density,
-                    inOutConfig, true /* legacyConfig */);
+                    inOutConfig, true /* overrideConfig */);
         }
 
         // It's possible that screen size will be considered in different orientation with or
@@ -9035,8 +9037,8 @@
             // vertically centered within parent bounds with insets, so position vertical bounds
             // within parent bounds with insets to prevent insets from unnecessarily trimming
             // vertical bounds.
-            final int bottom = Math.min(parentBoundsWithInsets.top + parentBounds.width() - 1,
-                    parentBoundsWithInsets.bottom);
+            final int bottom = Math.min(parentBoundsWithInsets.top
+                            + parentBoundsWithInsets.width() - 1, parentBoundsWithInsets.bottom);
             containingBounds.set(parentBounds.left, parentBoundsWithInsets.top, parentBounds.right,
                     bottom);
             containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top,
@@ -9047,8 +9049,8 @@
             // horizontally centered within parent bounds with insets, so position horizontal bounds
             // within parent bounds with insets to prevent insets from unnecessarily trimming
             // horizontal bounds.
-            final int right = Math.min(parentBoundsWithInsets.left + parentBounds.height(),
-                    parentBoundsWithInsets.right);
+            final int right = Math.min(parentBoundsWithInsets.left
+                            + parentBoundsWithInsets.height(), parentBoundsWithInsets.right);
             containingBounds.set(parentBoundsWithInsets.left, parentBounds.top, right,
                     parentBounds.bottom);
             containingBoundsWithInsets.set(parentBoundsWithInsets.left, parentBoundsWithInsets.top,
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 577d2a6..add0f1f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2302,7 +2302,7 @@
         }
 
         computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig,
-                false /* legacyConfig */);
+                false /* overrideConfig */);
 
         setDisplayInfoOverride();
 
@@ -2439,7 +2439,7 @@
         final DisplayCutout displayCutout = calculateDisplayCutoutForRotation(rotation);
         displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
         computeSizeRanges(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig,
-                false /* legacyConfig */);
+                false /* overrideConfig */);
         return displayInfo;
     }
 
@@ -2613,11 +2613,10 @@
      * @param dh Display Height in current rotation.
      * @param density Display density.
      * @param outConfig The output configuration to
-     * @param legacyConfig Whether we need to report the legacy result, which is excluding system
-     *                     decorations.
+     * @param overrideConfig Whether we need to report the override config result
      */
     void computeSizeRanges(DisplayInfo displayInfo, boolean rotated,
-            int dw, int dh, float density, Configuration outConfig, boolean legacyConfig) {
+            int dw, int dh, float density, Configuration outConfig, boolean overrideConfig) {
 
         // We need to determine the smallest width that will occur under normal
         // operation.  To this, start with the base screen size and compute the
@@ -2635,10 +2634,12 @@
         displayInfo.smallestNominalAppHeight = 1<<30;
         displayInfo.largestNominalAppWidth = 0;
         displayInfo.largestNominalAppHeight = 0;
-        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_0, unrotDw, unrotDh, legacyConfig);
-        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_90, unrotDh, unrotDw, legacyConfig);
-        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_180, unrotDw, unrotDh, legacyConfig);
-        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_270, unrotDh, unrotDw, legacyConfig);
+        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_0, unrotDw, unrotDh, overrideConfig);
+        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_90, unrotDh, unrotDw, overrideConfig);
+        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_180, unrotDw, unrotDh,
+                overrideConfig);
+        adjustDisplaySizeRanges(displayInfo, Surface.ROTATION_270, unrotDh, unrotDw,
+                overrideConfig);
 
         if (outConfig == null) {
             return;
@@ -2648,17 +2649,17 @@
     }
 
     private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh,
-            boolean legacyConfig) {
+            boolean overrideConfig) {
         final DisplayPolicy.DecorInsets.Info info = mDisplayPolicy.getDecorInsetsInfo(
                 rotation, dw, dh);
         final int w;
         final int h;
-        if (!legacyConfig) {
+        if (!overrideConfig) {
             w = info.mConfigFrame.width();
             h = info.mConfigFrame.height();
         } else {
-            w = info.mLegacyConfigFrame.width();
-            h = info.mLegacyConfigFrame.height();
+            w = info.mOverrideConfigFrame.width();
+            h = info.mOverrideConfigFrame.height();
         }
         if (w < displayInfo.smallestNominalAppWidth) {
             displayInfo.smallestNominalAppWidth = w;
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 90a9fee..3559ba8 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1919,9 +1919,9 @@
             final Rect mConfigInsets = new Rect();
 
             /**
-             * Legacy value of mConfigInsets for app compatibility purpose.
+             * Override value of mConfigInsets for app compatibility purpose.
              */
-            final Rect mLegacyConfigInsets = new Rect();
+            final Rect mOverrideConfigInsets = new Rect();
 
             /** The display frame available after excluding {@link #mNonDecorInsets}. */
             final Rect mNonDecorFrame = new Rect();
@@ -1934,9 +1934,9 @@
             final Rect mConfigFrame = new Rect();
 
             /**
-             * Legacy value of mConfigFrame for app compatibility purpose.
+             * Override value of mConfigFrame for app compatibility purpose.
              */
-            final Rect mLegacyConfigFrame = new Rect();
+            final Rect mOverrideConfigFrame = new Rect();
 
             private boolean mNeedUpdate = true;
 
@@ -1952,22 +1952,22 @@
                         ? decor
                         : insetsState.calculateInsets(displayFrame, dc.mWmService.mConfigTypes,
                                 true /* ignoreVisibility */);
-                final Insets legacyConfigInsets = dc.mWmService.mConfigTypes
-                        == dc.mWmService.mLegacyConfigTypes
+                final Insets overrideConfigInsets = dc.mWmService.mConfigTypes
+                        == dc.mWmService.mOverrideConfigTypes
                         ? configInsets
                         : insetsState.calculateInsets(displayFrame,
-                                dc.mWmService.mLegacyConfigTypes, true /* ignoreVisibility */);
+                                dc.mWmService.mOverrideConfigTypes, true /* ignoreVisibility */);
                 mNonDecorInsets.set(decor.left, decor.top, decor.right, decor.bottom);
                 mConfigInsets.set(configInsets.left, configInsets.top, configInsets.right,
                         configInsets.bottom);
-                mLegacyConfigInsets.set(legacyConfigInsets.left, legacyConfigInsets.top,
-                        legacyConfigInsets.right, legacyConfigInsets.bottom);
+                mOverrideConfigInsets.set(overrideConfigInsets.left, overrideConfigInsets.top,
+                        overrideConfigInsets.right, overrideConfigInsets.bottom);
                 mNonDecorFrame.set(displayFrame);
                 mNonDecorFrame.inset(mNonDecorInsets);
                 mConfigFrame.set(displayFrame);
                 mConfigFrame.inset(mConfigInsets);
-                mLegacyConfigFrame.set(displayFrame);
-                mLegacyConfigFrame.inset(mLegacyConfigInsets);
+                mOverrideConfigFrame.set(displayFrame);
+                mOverrideConfigFrame.inset(mOverrideConfigInsets);
                 mNeedUpdate = false;
                 return insetsState;
             }
@@ -1975,10 +1975,10 @@
             void set(Info other) {
                 mNonDecorInsets.set(other.mNonDecorInsets);
                 mConfigInsets.set(other.mConfigInsets);
-                mLegacyConfigInsets.set(other.mLegacyConfigInsets);
+                mOverrideConfigInsets.set(other.mOverrideConfigInsets);
                 mNonDecorFrame.set(other.mNonDecorFrame);
                 mConfigFrame.set(other.mConfigFrame);
-                mLegacyConfigFrame.set(other.mLegacyConfigFrame);
+                mOverrideConfigFrame.set(other.mOverrideConfigFrame);
                 mNeedUpdate = false;
             }
 
@@ -2091,7 +2091,7 @@
         final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh);
         final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh);
         if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)
-                && newInfo.mLegacyConfigFrame.equals(currentInfo.mLegacyConfigFrame)) {
+                && newInfo.mOverrideConfigFrame.equals(currentInfo.mOverrideConfigFrame)) {
             // Even if the config frame is not changed in current rotation, it may change the
             // insets in other rotations if the frame of insets source is changed.
             final InsetsState currentInsetsState = mDisplayContent.mDisplayFrames.mInsetsState;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 5d613cf..f8ae17c 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1216,6 +1216,10 @@
                     || mUserAspectRatio == USER_MIN_ASPECT_RATIO_FULLSCREEN);
     }
 
+    boolean hasFullscreenOverride() {
+        return isSystemOverrideToFullscreenEnabled() || shouldApplyUserFullscreenOverride();
+    }
+
     float getUserMinAspectRatio() {
         switch (mUserAspectRatio) {
             case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index e716c38..acffc65 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2449,7 +2449,7 @@
         if (!useLegacyInsetsForStableBounds) {
             intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mConfigInsets);
         } else {
-            intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mLegacyConfigInsets);
+            intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mOverrideConfigInsets);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8c6478d..395cd86 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -561,7 +561,7 @@
     /** Device default insets types shall be excluded from config app sizes. */
     final int mConfigTypes;
 
-    final int mLegacyConfigTypes;
+    final int mOverrideConfigTypes;
 
     final boolean mLimitedAlphaCompositing;
     final int mMaxUiWidth;
@@ -1195,20 +1195,18 @@
         if (mFlags.mInsetsDecoupledConfiguration) {
             mDecorTypes = 0;
             mConfigTypes = 0;
-        } else if (isScreenSizeDecoupledFromStatusBarAndCutout) {
-            mDecorTypes = WindowInsets.Type.navigationBars();
-            mConfigTypes = WindowInsets.Type.navigationBars();
         } else {
             mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars();
             mConfigTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.statusBars()
                     | WindowInsets.Type.navigationBars();
         }
-        if (isScreenSizeDecoupledFromStatusBarAndCutout) {
-            // Do not fallback to legacy value for enabled devices.
-            mLegacyConfigTypes = WindowInsets.Type.navigationBars();
+        if (isScreenSizeDecoupledFromStatusBarAndCutout && !mFlags.mInsetsDecoupledConfiguration) {
+            // If the global new behavior is not there, but the partial decouple flag is on.
+            mOverrideConfigTypes = 0;
         } else {
-            mLegacyConfigTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.statusBars()
-                    | WindowInsets.Type.navigationBars();
+            mOverrideConfigTypes =
+                    WindowInsets.Type.displayCutout() | WindowInsets.Type.statusBars()
+                            | WindowInsets.Type.navigationBars();
         }
 
         mLetterboxConfiguration = new LetterboxConfiguration(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 18f0311..8b23668 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2429,8 +2429,9 @@
         LogicalDisplay display =
                 logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
         assertThat(display.isEnabledLocked()).isFalse();
+        // TODO(b/332711269) make sure only one DISPLAY_GROUP_EVENT_ADDED sent.
         assertThat(callback.receivedEvents()).containsExactly(DISPLAY_GROUP_EVENT_ADDED,
-                EVENT_DISPLAY_CONNECTED).inOrder();
+                DISPLAY_GROUP_EVENT_ADDED, EVENT_DISPLAY_CONNECTED).inOrder();
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index 1a71e77..ea08be4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -317,7 +317,8 @@
                 mDisplayEventCaptor.capture());
         assertThat(mLogicalDisplayCaptor.getValue()).isEqualTo(mMockedLogicalDisplay);
         assertThat(mDisplayEventCaptor.getValue()).isEqualTo(EVENT_DISPLAY_CONNECTED);
-        verify(mMockedLogicalDisplay).setEnabledLocked(false);
+        verify(mMockedLogicalDisplayMapper).setDisplayEnabledLocked(eq(mMockedLogicalDisplay),
+                eq(false));
         clearInvocations(mMockedLogicalDisplayMapper);
         clearInvocations(mMockedLogicalDisplay);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
index 3aaac2e..65999ae 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceTest.java
@@ -196,6 +196,21 @@
     }
 
     @Test
+    @RequiresFlagsEnabled({Flags.FLAG_DE_HIDL, Flags.FLAG_FACE_VHAL_FEATURE})
+    public void registerAuthenticatorsLegacy_virtualFaceOnly() throws Exception {
+        initService();
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_FACE_VIRTUAL_ENABLED, 1);
+
+        mFaceService.mServiceWrapper.registerAuthenticatorsLegacy(mFaceSensorConfigurations);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL),
+                eq(BiometricAuthenticator.TYPE_FACE),
+                eq(Utils.propertyStrengthToAuthenticatorStrength(STRENGTH_STRONG)), any());
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
     public void registerAuthenticatorsLegacy_virtualAlwaysWhenNoOther() throws Exception {
         mFaceSensorConfigurations = new FaceSensorConfigurations(false);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
index 88956b6..cdbf1f7 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
@@ -243,6 +243,18 @@
     }
 
     @Test
+    public void registerAuthenticators_virtualFingerprintOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+        Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
+                Settings.Secure.BIOMETRIC_FINGERPRINT_VIRTUAL_ENABLED, 1);
+
+        mService.mServiceWrapper.registerAuthenticators(HIDL_AUTHENTICATORS);
+        waitForRegistration();
+
+        verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
+    }
+
+    @Test
     @RequiresFlagsEnabled(Flags.FLAG_DE_HIDL)
     public void registerAuthenticatorsLegacy_virtualOnly() throws Exception {
         initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index b96f39d..faa36a8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -129,6 +129,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
@@ -2326,6 +2327,92 @@
     }
 
     @Test
+    public void testUserOverrideFullscreenForLandscapeDisplay() {
+        final int displayWidth = 1600;
+        final int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mWmService.mLetterboxConfiguration);
+        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+                .isUserAppAspectRatioFullscreenEnabled();
+
+        // Set user aspect ratio override
+        spyOn(mActivity.mLetterboxUiController);
+        doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN).when(mActivity.mLetterboxUiController)
+                .getUserMinAspectRatioOverrideCode();
+
+        prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_PORTRAIT);
+
+        final Rect bounds = mActivity.getBounds();
+
+        // bounds should be fullscreen
+        assertEquals(displayHeight, bounds.height());
+        assertEquals(displayWidth, bounds.width());
+    }
+
+    @Test
+    public void testUserOverrideFullscreenForPortraitDisplay() {
+        final int displayWidth = 1400;
+        final int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mWmService.mLetterboxConfiguration);
+        doReturn(true).when(mActivity.mWmService.mLetterboxConfiguration)
+                .isUserAppAspectRatioFullscreenEnabled();
+
+        // Set user aspect ratio override
+        spyOn(mActivity.mLetterboxUiController);
+        doReturn(USER_MIN_ASPECT_RATIO_FULLSCREEN).when(mActivity.mLetterboxUiController)
+                .getUserMinAspectRatioOverrideCode();
+
+        prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_LANDSCAPE);
+
+        final Rect bounds = mActivity.getBounds();
+
+        // bounds should be fullscreen
+        assertEquals(displayHeight, bounds.height());
+        assertEquals(displayWidth, bounds.width());
+    }
+
+    @Test
+    public void testSystemFullscreenOverrideForLandscapeDisplay() {
+        final int displayWidth = 1600;
+        final int displayHeight = 1400;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mLetterboxUiController);
+        doReturn(true).when(mActivity.mLetterboxUiController)
+                .isSystemOverrideToFullscreenEnabled();
+
+        prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_PORTRAIT);
+
+        final Rect bounds = mActivity.getBounds();
+
+        // bounds should be fullscreen
+        assertEquals(displayHeight, bounds.height());
+        assertEquals(displayWidth, bounds.width());
+    }
+
+    @Test
+    public void testSystemFullscreenOverrideForPortraitDisplay() {
+        final int displayWidth = 1400;
+        final int displayHeight = 1600;
+        setUpDisplaySizeWithApp(displayWidth, displayHeight);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity.mLetterboxUiController);
+        doReturn(true).when(mActivity.mLetterboxUiController)
+                .isSystemOverrideToFullscreenEnabled();
+
+        prepareMinAspectRatio(mActivity, 16 / 9f, SCREEN_ORIENTATION_LANDSCAPE);
+
+        final Rect bounds = mActivity.getBounds();
+
+        // bounds should be fullscreen
+        assertEquals(displayHeight, bounds.height());
+        assertEquals(displayWidth, bounds.width());
+    }
+
+    @Test
     public void testUserOverrideSplitScreenAspectRatioForLandscapeDisplay() {
         final int displayWidth = 1600;
         final int displayHeight = 1400;
@@ -4075,6 +4162,7 @@
     }
 
     @Test
+    @Ignore // TODO(b/330888878): fix test in main
     public void testPortraitCloseToSquareDisplayWithTaskbar_notLetterboxed() {
         // Set up portrait close to square display
         setUpDisplaySizeWithApp(2200, 2280);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 03d9871..7bba5e8 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9872,6 +9872,50 @@
      */
     public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING =
             "satellite_information_redirect_url_string";
+    /**
+     * Indicate whether a carrier supports emergency messaging. When this config is {@code  false},
+     * emergency call to satellite T911 handover will be disabled.
+     *
+     * This will need agreement with carriers before enabling this flag.
+     *
+     * The default value is false.
+     *
+     * @hide
+     */
+    public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL =
+            "emergency_messaging_supported_bool";
+
+    /**
+     * An integer key holds the timeout duration in milliseconds used to determine whether to hand
+     * over an emergency call to satellite T911.
+     *
+     * The timer is started when there is an ongoing emergency call, and the IMS is not registered,
+     * and cellular service is not available. When the timer expires,
+     * {@link com.android.internal.telephony.satellite.SatelliteSOSMessageRecommender} will send the
+     * event {@link TelephonyManager#EVENT_DISPLAY_EMERGENCY_MESSAGE} to Dialer, which will then
+     * prompt user to switch to using satellite emergency messaging.
+     *
+     * The default value is 30 seconds.
+     *
+     * @hide
+     */
+    public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT =
+            "emergency_call_to_satellite_t911_handover_timeout_millis_int";
+
+    /**
+     * An int array that contains default capabilities for carrier enabled satellite roaming.
+     * If any PLMN is provided from the entitlement server, and it is not listed in
+     * {@link #KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE}, default capabilities
+     * will be used instead.
+     * <p>
+     * The default capabilities are
+     * {@link NetworkRegistrationInfo#SERVICE_TYPE_SMS}, and
+     * {@link NetworkRegistrationInfo#SERVICE_TYPE_MMS}
+     *
+     * @hide
+     */
+    public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY =
+            "carrier_roaming_satellite_default_services_int_array";
 
     /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
@@ -10993,7 +11037,15 @@
         sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
         sDefaults.putString(KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING, "androidSatmode");
         sDefaults.putString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, "");
+        sDefaults.putIntArray(KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY,
+                new int[] {
+                        NetworkRegistrationInfo.SERVICE_TYPE_SMS,
+                        NetworkRegistrationInfo.SERVICE_TYPE_MMS
+                });
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
+        sDefaults.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false);
+        sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT,
+                (int) TimeUnit.SECONDS.toMillis(30));
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
         sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
index 06fc3c6..579fda3 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl
@@ -26,11 +26,13 @@
     /**
      * Called when satellite datagram send state changed.
      *
+     * @param datagramType The datagram type of currently being sent.
      * @param state The new send datagram transfer state.
      * @param sendPendingCount The number of datagrams that are currently being sent.
      * @param errorCode If datagram transfer failed, the reason for failure.
      */
-    void onSendDatagramStateChanged(in int state, in int sendPendingCount, in int errorCode);
+    void onSendDatagramStateChanged(int datagramType, int state, int sendPendingCount,
+            int errorCode);
 
     /**
      * Called when satellite datagram receive state changed.
@@ -39,7 +41,7 @@
      * @param receivePendingCount The number of datagrams that are currently pending to be received.
      * @param errorCode If datagram transfer failed, the reason for failure.
      */
-    void onReceiveDatagramStateChanged(in int state, in int receivePendingCount, in int errorCode);
+    void onReceiveDatagramStateChanged(int state, int receivePendingCount, int errorCode);
 
     /**
      * Called when the satellite position changed.
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 20b24b9..87bb0f0 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -992,12 +992,19 @@
      */
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2;
+    /**
+     * This type of datagram is used to keep the device in satellite connected state or check if
+     * there is any incoming message.
+     * @hide
+     */
+    public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3;
 
     /** @hide */
     @IntDef(prefix = "DATAGRAM_TYPE_", value = {
             DATAGRAM_TYPE_UNKNOWN,
             DATAGRAM_TYPE_SOS_MESSAGE,
-            DATAGRAM_TYPE_LOCATION_SHARING
+            DATAGRAM_TYPE_LOCATION_SHARING,
+            DATAGRAM_TYPE_KEEP_ALIVE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface DatagramType {}
@@ -1077,8 +1084,13 @@
                             }
 
                             @Override
-                            public void onSendDatagramStateChanged(int state, int sendPendingCount,
-                                    int errorCode) {
+                            public void onSendDatagramStateChanged(int datagramType, int state,
+                                    int sendPendingCount, int errorCode) {
+                                executor.execute(() -> Binder.withCleanCallingIdentity(
+                                        () -> callback.onSendDatagramStateChanged(datagramType,
+                                                state, sendPendingCount, errorCode)));
+
+                                // For backward compatibility
                                 executor.execute(() -> Binder.withCleanCallingIdentity(
                                         () -> callback.onSendDatagramStateChanged(
                                                 state, sendPendingCount, errorCode)));
diff --git a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
index e020970..d8bd662 100644
--- a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java
@@ -52,6 +52,19 @@
             @SatelliteManager.SatelliteResult int errorCode);
 
     /**
+     * Called when satellite datagram send state changed.
+     *
+     * @param datagramType The datagram type of currently being sent.
+     * @param state The new send datagram transfer state.
+     * @param sendPendingCount The number of datagrams that are currently being sent.
+     * @param errorCode If datagram transfer failed, the reason for failure.
+     *
+     * @hide
+     */
+    void onSendDatagramStateChanged(@SatelliteManager.DatagramType int datagramType,
+            @SatelliteManager.SatelliteDatagramTransferState int state, int sendPendingCount,
+            @SatelliteManager.SatelliteResult int errorCode);
+    /**
      * Called when satellite datagram receive state changed.
      *
      * @param state The new receive datagram transfer state.