Merge "Implement the method for time short string" into 24D1-dev
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index e35e801..4106863 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -346,10 +346,10 @@
     @Override
     public String toString() {
         return "DeviceState{" + "identifier=" + mIdentifier + ", name='" + mName + '\''
-                + ", app_accessible=" + !hasProperty(PROPERTY_APP_INACCESSIBLE)
-                + ", cancel_when_requester_not_on_top="
-                + hasProperty(PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP)
-                + "}";
+                + ", app_accessible=" + !(hasProperty(PROPERTY_APP_INACCESSIBLE)
+                || hasFlag(FLAG_APP_INACCESSIBLE)) + ", cancel_when_requester_not_on_top="
+                + (hasProperty(PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP)
+                || hasFlag(FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP)) + "}";
     }
 
     @Override
diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
index 01b4569..e5ef833 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -229,7 +229,8 @@
                     + "gluedLayoutDirection = " + mGluedLayoutDirection);
         }
 
-        if (layoutDirection != mGluedLayoutDirection) {
+        final boolean alreadyGlued = mGluedLayoutDirection != LAYOUT_DIRECTION_UNDEFINED;
+        if (alreadyGlued && layoutDirection != mGluedLayoutDirection) {
             if (DEBUG_NEW_ACTION_LAYOUT) {
                 Log.d(TAG, "onRtlPropertiesChanged: layout direction changed; regluing");
             }
@@ -249,14 +250,6 @@
             return;
         }
 
-        if (mIconToGlue == null && mLabelToGlue == null) {
-            if (DEBUG_NEW_ACTION_LAYOUT) {
-                Log.v(TAG, "glueIconAndLabelIfNeeded: no icon or label to glue; doing nothing");
-            }
-            mGluePending = false;
-            return;
-        }
-
         if (!evenlyDividedCallStyleActionLayout()) {
             Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing");
             return;
@@ -272,22 +265,6 @@
             return;
         }
 
-        // Ready to glue but don't have an icon *and* a label:
-        //
-        // (Note that this will *not* happen while the button is being initialized, since we won't
-        // be ready to glue. This can only happen if the button is initialized and displayed and
-        // *then* someone calls glueIcon or glueLabel.
-
-        if (mIconToGlue == null) {
-            Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing");
-            return;
-        }
-
-        if (mLabelToGlue == null) {
-            Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing");
-            return;
-        }
-
         // Can't glue:
 
         final int layoutDirection = getLayoutDirection();
@@ -318,6 +295,28 @@
     private static final String POP_DIRECTIONAL_ISOLATE = "\u2069";
 
     private void glueIconAndLabel(int layoutDirection) {
+        if (mIconToGlue == null && mLabelToGlue == null) {
+            if (DEBUG_NEW_ACTION_LAYOUT) {
+                Log.d(TAG, "glueIconAndLabel: null icon and label, setting text to empty string");
+            }
+            setText("");
+            return;
+        } else if (mIconToGlue == null) {
+            if (DEBUG_NEW_ACTION_LAYOUT) {
+                Log.d(TAG, "glueIconAndLabel: null icon, setting text to label");
+            }
+            setText(mLabelToGlue);
+            return;
+        } else if (mLabelToGlue == null) {
+            if (DEBUG_NEW_ACTION_LAYOUT) {
+                Log.d(TAG, "glueIconAndLabel: null label, setting text to ImageSpan with icon");
+            }
+            final SpannableStringBuilder builder = new SpannableStringBuilder();
+            appendSpan(builder, IMAGE_SPAN_TEXT, new ImageSpan(mIconToGlue, ALIGN_CENTER));
+            setText(builder);
+            return;
+        }
+
         final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL;
 
         if (DEBUG_NEW_ACTION_LAYOUT) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
index adf4fc6..b253309 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -140,6 +140,14 @@
     }
 
     @Test
+    fun canShowAlternateBouncerForFingerprint_primaryBouncerShowing() {
+        givenCanShowAlternateBouncer()
+        bouncerRepository.setPrimaryShow(true)
+
+        assertFalse(underTest.canShowAlternateBouncerForFingerprint())
+    }
+
+    @Test
     fun show_whenCannotShow() {
         givenCannotShowAlternateBouncer()
 
@@ -202,7 +210,7 @@
         } else {
             bouncerRepository.setAlternateBouncerUIAvailable(true)
         }
-
+        bouncerRepository.setPrimaryShow(false)
         biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
         biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
         whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
diff --git a/packages/SystemUI/res/layout/notif_half_shelf.xml b/packages/SystemUI/res/layout/notif_half_shelf.xml
index 68c8dd9..d8d2985 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf.xml
@@ -19,11 +19,11 @@
         xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
         android:id="@+id/half_shelf_dialog"
         android:orientation="vertical"
-        android:layout_width="wrap_content"
+        android:layout_width="@dimen/large_dialog_width"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal|bottom"
-        android:paddingStart="4dp"
-        android:paddingEnd="4dp">
+        android:paddingLeft="@dimen/dialog_side_padding"
+        android:paddingRight="@dimen/dialog_side_padding">
 
     <LinearLayout
         android:id="@+id/half_shelf"
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index af32eb5..000f03a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -109,7 +109,8 @@
             biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed.value &&
             !keyguardUpdateMonitor.isFingerprintLockedOut &&
             !keyguardStateController.isUnlocked &&
-            !statusBarStateController.isDozing
+            !statusBarStateController.isDozing &&
+            !bouncerRepository.primaryBouncerShow.value
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
index ab8fc65..12bff49 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/AssistContentRequester.java
@@ -15,6 +15,7 @@
  */
 
 package com.android.systemui.screenshot;
+import android.annotation.Nullable;
 import android.app.ActivityTaskManager;
 import android.app.IActivityTaskManager;
 import android.app.IAssistDataReceiver;
@@ -55,7 +56,7 @@
          * Called when the {@link android.app.assist.AssistContent} of the requested task is
          * available.
          **/
-        void onAssistContentAvailable(AssistContent assistContent);
+        void onAssistContentAvailable(@Nullable AssistContent assistContent);
     }
 
     private final IActivityTaskManager mActivityTaskManager;
@@ -117,15 +118,9 @@
 
         @Override
         public void onHandleAssistData(Bundle data) {
-            if (data == null) {
-                return;
-            }
-
-            final AssistContent content = data.getParcelable(ASSIST_KEY_CONTENT);
-            if (content == null) {
-                Log.e(TAG, "Received AssistData, but no AssistContent found");
-                return;
-            }
+            final AssistContent content = (data == null) ? null
+                    : data.getParcelable(
+                            ASSIST_KEY_CONTENT, AssistContent.class);
 
             AssistContentRequester requester = mParentRef.get();
             if (requester != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ReferenceScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/ReferenceScreenshotModule.java
index 6224e1b..afb0280 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ReferenceScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ReferenceScreenshotModule.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenshot;
 
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 
@@ -29,4 +30,9 @@
     static ScreenshotNotificationSmartActionsProvider providesScrnshtNotifSmartActionsProvider() {
         return new ScreenshotNotificationSmartActionsProvider();
     }
+
+    /** */
+    @Binds
+    ScreenshotActionsProvider.Factory bindScreenshotActionsProviderFactory(
+            DefaultScreenshotActionsProvider.Factory defaultScreenshotActionsProviderFactory);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
index f69021f..07e143a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -28,8 +28,7 @@
 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.ActionButtonAppearance
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -43,7 +42,11 @@
     fun onScrollChipReady(onClick: Runnable)
     fun setCompletedScreenshot(result: ScreenshotSavedResult)
 
-    fun onAssistContentAvailable(assistContent: AssistContent) {}
+    /**
+     * Provide the AssistContent for the focused task if available, null if the focused task isn't
+     * known or didn't return data.
+     */
+    fun onAssistContent(assistContent: AssistContent?) {}
 
     interface Factory {
         fun create(
@@ -59,7 +62,6 @@
 constructor(
     private val context: Context,
     private val viewModel: ScreenshotViewModel,
-    private val smartActionsProvider: SmartActionsProvider,
     private val uiEventLogger: UiEventLogger,
     @Assisted val request: ScreenshotData,
     @Assisted val requestId: String,
@@ -81,81 +83,52 @@
             }
         }
         viewModel.addAction(
-            ActionButtonViewModel(
+            ActionButtonAppearance(
                 AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
                 context.resources.getString(R.string.screenshot_edit_label),
                 context.resources.getString(R.string.screenshot_edit_description),
-            ) {
-                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
-                    )
-                }
+            )
+        ) {
+            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
+                )
             }
-        )
+        }
+
         viewModel.addAction(
-            ActionButtonViewModel(
+            ActionButtonAppearance(
                 AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
                 context.resources.getString(R.string.screenshot_share_label),
                 context.resources.getString(R.string.screenshot_share_description),
-            ) {
-                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
-                    )
-                }
-            }
-        )
-        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)
-                        }
-                    }
+            )
+        ) {
+            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
                 )
-            } else {
-                Log.w(TAG, "Received immutable quick share pending intent; ignoring")
             }
         }
     }
 
     override fun onScrollChipReady(onClick: Runnable) {
         viewModel.addAction(
-            ActionButtonViewModel(
+            ActionButtonAppearance(
                 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()
-            }
-        )
+            )
+        ) {
+            onClick.run()
+        }
     }
 
     override fun setCompletedScreenshot(result: ScreenshotSavedResult) {
@@ -165,15 +138,6 @@
         }
         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) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index f43882c..1428e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -176,11 +176,12 @@
 
     // These strings are used for communicating the action invoked to
     // ScreenshotNotificationSmartActionsProvider.
-    static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
-    static final String EXTRA_ID = "android:screenshot_id";
-    static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
-    static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
-    static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin";
+    public static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
+    public static final String EXTRA_ID = "android:screenshot_id";
+    public static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
+    public static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
+    public static final String EXTRA_ACTION_INTENT_FILLIN =
+            "android:screenshot_action_intent_fillin";
 
 
     // From WizardManagerHelper.java
@@ -411,9 +412,9 @@
 
             if (screenshot.getTaskId() >= 0) {
                 mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
-                        assistContent -> {
-                            mActionsProvider.onAssistContentAvailable(assistContent);
-                        });
+                        assistContent -> mActionsProvider.onAssistContent(assistContent));
+            } else {
+                mActionsProvider.onAssistContent(null);
             }
         } else {
             saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
index 3eafbfb..23f05e0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java
@@ -44,7 +44,7 @@
     public static final String DEFAULT_ACTION_TYPE = "Smart Action";
 
     /* Define phases of screenshot execution. */
-    protected enum ScreenshotOp {
+    public enum ScreenshotOp {
         OP_UNKNOWN,
         RETRIEVE_SMART_ACTIONS,
         REQUEST_SMART_ACTIONS,
@@ -52,7 +52,7 @@
     }
 
     /* Enum to report success or failure for screenshot execution phases. */
-    protected enum ScreenshotOpStatus {
+    public enum ScreenshotOpStatus {
         OP_STATUS_UNKNOWN,
         SUCCESS,
         ERROR,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 6b9332b..254c133 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -28,6 +28,7 @@
 import android.view.View
 import android.view.ViewTreeObserver
 import android.view.WindowInsets
+import android.view.WindowManager
 import android.window.OnBackInvokedCallback
 import android.window.OnBackInvokedDispatcher
 import com.android.internal.logging.UiEventLogger
@@ -53,6 +54,7 @@
 constructor(
     private val logger: UiEventLogger,
     private val viewModel: ScreenshotViewModel,
+    private val windowManager: WindowManager,
     @Assisted private val context: Context,
     @Assisted private val displayId: Int
 ) : ScreenshotViewProxy {
@@ -79,6 +81,16 @@
         addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
         setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
         debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
+        view.viewTreeObserver.addOnComputeInternalInsetsListener { info ->
+            val touchableRegion =
+                view.getTouchRegion(
+                    windowManager.currentWindowMetrics.windowInsets.getInsets(
+                        WindowInsets.Type.systemGestures()
+                    )
+                )
+            info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
+            info.touchableRegion.set(touchableRegion)
+        }
         screenshotPreview = view.screenshotPreview
     }
 
@@ -194,6 +206,7 @@
             }
         )
     }
+
     private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
         view.setOnKeyListener(
             object : View.OnKeyListener {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
deleted file mode 100644
index a895b30..0000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * 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/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 2ce6d83..b3eaa91 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -22,12 +22,10 @@
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.screenshot.DefaultScreenshotActionsProvider;
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
 import com.android.systemui.screenshot.RequestProcessor;
-import com.android.systemui.screenshot.ScreenshotActionsProvider;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotProxyService;
@@ -93,10 +91,6 @@
     abstract ScreenshotSoundController bindScreenshotSoundController(
             ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
 
-    @Binds
-    abstract ScreenshotActionsProvider.Factory bindScreenshotActionsProviderFactory(
-            DefaultScreenshotActionsProvider.Factory defaultScreenshotActionsProviderFactory);
-
     @Provides
     @SysUISingleton
     static ScreenshotViewModel providesScreenshotViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
index 747ad4f..b7a03ef 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -17,17 +17,70 @@
 package com.android.systemui.screenshot.ui
 
 import android.content.Context
+import android.graphics.Insets
+import android.graphics.Rect
+import android.graphics.Region
 import android.util.AttributeSet
+import android.view.View
 import android.widget.ImageView
 import androidx.constraintlayout.widget.ConstraintLayout
 import com.android.systemui.res.R
+import com.android.systemui.screenshot.FloatingWindowUtil
 
 class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
     ConstraintLayout(context, attrs) {
     lateinit var screenshotPreview: ImageView
 
+    private val displayMetrics = context.resources.displayMetrics
+    private val tmpRect = Rect()
+    private lateinit var actionsContainerBackground: View
+    private lateinit var dismissButton: View
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         screenshotPreview = requireViewById(R.id.screenshot_preview)
+        actionsContainerBackground = requireViewById(R.id.actions_container_background)
+        dismissButton = requireViewById(R.id.screenshot_dismiss_button)
+    }
+
+    fun getTouchRegion(gestureInsets: Insets): Region {
+        val region = getSwipeRegion()
+
+        // Receive touches in gesture insets so they don't cause TOUCH_OUTSIDE
+        // left edge gesture region
+        val insetRect = Rect(0, 0, gestureInsets.left, displayMetrics.heightPixels)
+        region.op(insetRect, Region.Op.UNION)
+        // right edge gesture region
+        insetRect.set(
+            displayMetrics.widthPixels - gestureInsets.right,
+            0,
+            displayMetrics.widthPixels,
+            displayMetrics.heightPixels
+        )
+        region.op(insetRect, Region.Op.UNION)
+
+        return region
+    }
+
+    private fun getSwipeRegion(): Region {
+        val swipeRegion = Region()
+        val padding = FloatingWindowUtil.dpToPx(displayMetrics, -1 * TOUCH_PADDING_DP).toInt()
+        swipeRegion.addInsetView(screenshotPreview, padding)
+        swipeRegion.addInsetView(actionsContainerBackground, padding)
+        swipeRegion.addInsetView(dismissButton, padding)
+        findViewById<View>(R.id.screenshot_message_container)?.let {
+            swipeRegion.addInsetView(it, padding)
+        }
+        return swipeRegion
+    }
+
+    private fun Region.addInsetView(view: View, padding: Int = 0) {
+        view.getBoundsOnScreen(tmpRect)
+        tmpRect.inset(padding, padding)
+        this.op(tmpRect, Region.Op.UNION)
+    }
+
+    companion object {
+        private const val TOUCH_PADDING_DP = 12f
     }
 }
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 a6374ae..3c5a0ec 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
@@ -28,16 +28,16 @@
     fun bind(view: View, viewModel: ActionButtonViewModel) {
         val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
         val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
-        iconView.setImageDrawable(viewModel.icon)
-        textView.text = viewModel.name
-        setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false)
+        iconView.setImageDrawable(viewModel.appearance.icon)
+        textView.text = viewModel.appearance.label
+        setMargins(iconView, textView, viewModel.appearance.label?.isNotEmpty() ?: false)
         if (viewModel.onClicked != null) {
             view.setOnClickListener { viewModel.onClicked.invoke() }
         } else {
             view.setOnClickListener(null)
         }
         view.tag = viewModel.id
-        view.contentDescription = viewModel.description
+        view.contentDescription = viewModel.appearance.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 32e9296..d9a5102 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
@@ -65,7 +65,9 @@
                     }
                     launch {
                         viewModel.actions.collect { actions ->
-                            if (actions.isNotEmpty()) {
+                            val visibleActions = actions.filter { it.visible }
+
+                            if (visibleActions.isNotEmpty()) {
                                 view
                                     .requireViewById<View>(R.id.actions_container_background)
                                     .visibility = View.VISIBLE
@@ -75,7 +77,7 @@
                             // 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 }
+                            val newIds = visibleActions.map { it.id }
 
                             for (view in actionsContainer.children.toList()) {
                                 if (view.tag !in newIds) {
@@ -83,7 +85,7 @@
                                 }
                             }
 
-                            for ((index, action) in actions.withIndex()) {
+                            for ((index, action) in visibleActions.withIndex()) {
                                 val currentView: View? = actionsContainer.getChildAt(index)
                                 if (action.id == currentView?.tag) {
                                     // Same ID, update the display
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonAppearance.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonAppearance.kt
new file mode 100644
index 0000000..55a2ad2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonAppearance.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+/** Data describing how an action should be shown to the user. */
+data class ActionButtonAppearance(
+    val icon: Drawable?,
+    val label: CharSequence?,
+    val description: CharSequence,
+)
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 97b24c1..c5fa8db 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
@@ -16,19 +16,20 @@
 
 package com.android.systemui.screenshot.ui.viewmodel
 
-import android.graphics.drawable.Drawable
-
 data class ActionButtonViewModel(
-    val icon: Drawable?,
-    val name: CharSequence?,
-    val description: CharSequence,
+    val appearance: ActionButtonAppearance,
+    val id: Int,
+    val visible: Boolean,
     val onClicked: (() -> Unit)?,
 ) {
-    val id: Int = getId()
-
     companion object {
         private var nextId = 0
 
         private fun getId() = nextId.also { nextId += 1 }
+
+        fun withNextId(
+            appearance: ActionButtonAppearance,
+            onClicked: (() -> Unit)?
+        ): ActionButtonViewModel = ActionButtonViewModel(appearance, getId(), true, onClicked)
     }
 }
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 ddfa69b..f67ad40 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
@@ -17,6 +17,7 @@
 package com.android.systemui.screenshot.ui.viewmodel
 
 import android.graphics.Bitmap
+import android.util.Log
 import android.view.accessibility.AccessibilityManager
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -39,16 +40,56 @@
         _previewAction.value = onClick
     }
 
-    fun addAction(action: ActionButtonViewModel) {
+    fun addAction(actionAppearance: ActionButtonAppearance, onClicked: (() -> Unit)): Int {
         val actionList = _actions.value.toMutableList()
+        val action = ActionButtonViewModel.withNextId(actionAppearance, onClicked)
         actionList.add(action)
         _actions.value = actionList
+        return action.id
     }
 
-    fun addActions(actions: List<ActionButtonViewModel>) {
+    fun setActionVisibility(actionId: Int, visible: Boolean) {
         val actionList = _actions.value.toMutableList()
-        actionList.addAll(actions)
-        _actions.value = actionList
+        val index = actionList.indexOfFirst { it.id == actionId }
+        if (index >= 0) {
+            actionList[index] =
+                ActionButtonViewModel(
+                    actionList[index].appearance,
+                    actionId,
+                    visible,
+                    actionList[index].onClicked
+                )
+            _actions.value = actionList
+        } else {
+            Log.w(TAG, "Attempted to update unknown action id $actionId")
+        }
+    }
+
+    fun updateActionAppearance(actionId: Int, appearance: ActionButtonAppearance) {
+        val actionList = _actions.value.toMutableList()
+        val index = actionList.indexOfFirst { it.id == actionId }
+        if (index >= 0) {
+            actionList[index] =
+                ActionButtonViewModel(
+                    appearance,
+                    actionId,
+                    actionList[index].visible,
+                    actionList[index].onClicked
+                )
+            _actions.value = actionList
+        } else {
+            Log.w(TAG, "Attempted to update unknown action id $actionId")
+        }
+    }
+
+    fun removeAction(actionId: Int) {
+        val actionList = _actions.value.toMutableList()
+        if (actionList.removeIf { it.id == actionId }) {
+            // Update if something was removed.
+            _actions.value = actionList
+        } else {
+            Log.w(TAG, "Attempted to remove unknown action id $actionId")
+        }
     }
 
     fun reset() {
@@ -56,4 +97,8 @@
         _previewAction.value = null
         _actions.value = listOf()
     }
+
+    companion object {
+        const val TAG = "ScreenshotViewModel"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 27db84f..c7ef243 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -51,6 +51,7 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.provider.Settings;
 import android.util.AttributeSet;
@@ -221,6 +222,9 @@
     private float mQsExpansionFraction;
     private final int mSplitShadeMinContentHeight;
     private String mLastUpdateSidePaddingDumpString;
+    private long mLastUpdateSidePaddingElapsedRealtime;
+    private String mLastInitViewDumpString;
+    private long mLastInitViewElapsedRealtime;
 
     /**
      * The algorithm which calculates the properties for our children
@@ -1094,17 +1098,34 @@
         mOverflingDistance = configuration.getScaledOverflingDistance();
 
         Resources res = context.getResources();
+        final boolean isSmallScreenLandscape = res.getBoolean(R.bool.is_small_screen_landscape);
         boolean useSmallLandscapeLockscreenResources = mIsSmallLandscapeLockscreenEnabled
-                && res.getBoolean(R.bool.is_small_screen_landscape);
+                && isSmallScreenLandscape;
         // TODO (b/293252410) remove condition here when flag is launched
         //  Instead update the config_skinnyNotifsInLandscape to be false whenever
         //  is_small_screen_landscape is true. Then, only use the config_skinnyNotifsInLandscape.
+        final boolean configSkinnyNotifsInLandscape = res.getBoolean(
+                R.bool.config_skinnyNotifsInLandscape);
         if (useSmallLandscapeLockscreenResources) {
             mSkinnyNotifsInLandscape = false;
         } else {
-            mSkinnyNotifsInLandscape = res.getBoolean(
-                    R.bool.config_skinnyNotifsInLandscape);
+            mSkinnyNotifsInLandscape = configSkinnyNotifsInLandscape;
         }
+
+        mLastInitViewDumpString =
+                "mIsSmallLandscapeLockscreenEnabled=" + mIsSmallLandscapeLockscreenEnabled
+                        + " isSmallScreenLandscape=" + isSmallScreenLandscape
+                        + " useSmallLandscapeLockscreenResources="
+                        + useSmallLandscapeLockscreenResources
+                        + " skinnyNotifsInLandscape=" + configSkinnyNotifsInLandscape
+                        + " mSkinnyNotifsInLandscape=" + mSkinnyNotifsInLandscape;
+        mLastInitViewElapsedRealtime = SystemClock.elapsedRealtime();
+
+        if (DEBUG_UPDATE_SIDE_PADDING) {
+            Log.v(TAG, "initView @ elapsedRealtime " + mLastInitViewElapsedRealtime + ": "
+                    + mLastInitViewDumpString);
+        }
+
         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
         mStackScrollAlgorithm.initView(context);
         mStateAnimator.initView(context);
@@ -1127,24 +1148,34 @@
     }
 
     void updateSidePadding(int viewWidth) {
-        final boolean portrait =
-                getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
+        final int orientation = getResources().getConfiguration().orientation;
 
         mLastUpdateSidePaddingDumpString = "viewWidth=" + viewWidth
                 + " skinnyNotifsInLandscape=" + mSkinnyNotifsInLandscape
-                + " portrait=" + portrait;
+                + " orientation=" + orientation;
+        mLastUpdateSidePaddingElapsedRealtime = SystemClock.elapsedRealtime();
 
         if (DEBUG_UPDATE_SIDE_PADDING) {
-            Log.v(TAG, "updateSidePadding: " + mLastUpdateSidePaddingDumpString);
+            Log.v(TAG,
+                    "updateSidePadding @ elapsedRealtime " + mLastUpdateSidePaddingElapsedRealtime
+                            + ": " + mLastUpdateSidePaddingDumpString);
         }
 
-        if (viewWidth == 0 || !mSkinnyNotifsInLandscape) {
+        if (viewWidth == 0) {
+            Log.e(TAG, "updateSidePadding: viewWidth is zero");
             mSidePaddings = mMinimumPaddings;
             return;
         }
 
-        // Portrait is easy, just use the dimen for paddings
-        if (portrait) {
+        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+            mSidePaddings = mMinimumPaddings;
+            return;
+        }
+
+        if (mShouldUseSplitNotificationShade) {
+            if (mSkinnyNotifsInLandscape) {
+                Log.e(TAG, "updateSidePadding: mSkinnyNotifsInLandscape has betrayed us!");
+            }
             mSidePaddings = mMinimumPaddings;
             return;
         }
@@ -5313,6 +5344,7 @@
 
     public void dump(PrintWriter pwOriginal, String[] args) {
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
+        final long elapsedRealtime = SystemClock.elapsedRealtime();
         pw.println("Internal state:");
         DumpUtilsKt.withIncreasedIndent(pw, () -> {
             println(pw, "pulsing", mPulsing);
@@ -5343,7 +5375,17 @@
             println(pw, "minimumPaddings", mMinimumPaddings);
             println(pw, "qsTilePadding", mQsTilePadding);
             println(pw, "sidePaddings", mSidePaddings);
+            println(pw, "elapsedRealtime", elapsedRealtime);
+            println(pw, "lastInitView", mLastInitViewDumpString);
+            println(pw, "lastInitViewElapsedRealtime", mLastInitViewElapsedRealtime);
+            println(pw, "lastInitViewMillisAgo", elapsedRealtime - mLastInitViewElapsedRealtime);
+            println(pw, "shouldUseSplitNotificationShade", mShouldUseSplitNotificationShade);
             println(pw, "lastUpdateSidePadding", mLastUpdateSidePaddingDumpString);
+            println(pw, "lastUpdateSidePaddingElapsedRealtime",
+                    mLastUpdateSidePaddingElapsedRealtime);
+            println(pw, "lastUpdateSidePaddingMillisAgo",
+                    elapsedRealtime - mLastUpdateSidePaddingElapsedRealtime);
+            println(pw, "isSmallLandscapeLockscreenEnabled", mIsSmallLandscapeLockscreenEnabled);
             mNotificationStackSizeCalculator.dump(pw, args);
         });
         pw.println();
@@ -5922,6 +5964,7 @@
             mAmbientState.setUseSplitShade(split);
             updateDismissBehavior();
             updateUseRoundedRectClipping();
+            requestLayout();
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
index bde821b..853e50a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
@@ -16,8 +16,6 @@
 
 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
@@ -27,8 +25,6 @@
 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
@@ -41,10 +37,7 @@
 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
@@ -52,7 +45,6 @@
     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)
@@ -119,42 +111,10 @@
         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",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt
new file mode 100644
index 0000000..d44e26c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.ui.viewmodel
+
+import android.view.accessibility.AccessibilityManager
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.mock
+
+@SmallTest
+class ScreenshotViewModelTest {
+    private val accessibilityManager: AccessibilityManager = mock(AccessibilityManager::class.java)
+    private val appearance = ActionButtonAppearance(null, "Label", "Description")
+    private val onclick = {}
+
+    @Test
+    fun testAddAction() {
+        val viewModel = ScreenshotViewModel(accessibilityManager)
+
+        assertThat(viewModel.actions.value).isEmpty()
+
+        viewModel.addAction(appearance, onclick)
+
+        assertThat(viewModel.actions.value).hasSize(1)
+
+        val added = viewModel.actions.value[0]
+        assertThat(added.appearance).isEqualTo(appearance)
+        assertThat(added.onClicked).isEqualTo(onclick)
+    }
+
+    @Test
+    fun testRemoveAction() {
+        val viewModel = ScreenshotViewModel(accessibilityManager)
+        val firstId = viewModel.addAction(ActionButtonAppearance(null, "", ""), {})
+        val secondId = viewModel.addAction(appearance, onclick)
+
+        assertThat(viewModel.actions.value).hasSize(2)
+        assertThat(firstId).isNotEqualTo(secondId)
+
+        viewModel.removeAction(firstId)
+
+        assertThat(viewModel.actions.value).hasSize(1)
+
+        val remaining = viewModel.actions.value[0]
+        assertThat(remaining.appearance).isEqualTo(appearance)
+        assertThat(remaining.onClicked).isEqualTo(onclick)
+    }
+
+    @Test
+    fun testUpdateActionAppearance() {
+        val viewModel = ScreenshotViewModel(accessibilityManager)
+        val id = viewModel.addAction(appearance, onclick)
+        val otherAppearance = ActionButtonAppearance(null, "Other", "Other")
+
+        viewModel.updateActionAppearance(id, otherAppearance)
+
+        assertThat(viewModel.actions.value).hasSize(1)
+        val updated = viewModel.actions.value[0]
+        assertThat(updated.appearance).isEqualTo(otherAppearance)
+        assertThat(updated.onClicked).isEqualTo(onclick)
+    }
+}
diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
index 23e269a..cbbce1a 100644
--- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
+++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java
@@ -19,6 +19,7 @@
 import static android.app.WallpaperManager.FLAG_LOCK;
 import static android.app.WallpaperManager.FLAG_SYSTEM;
 import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
 
 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_INELIGIBLE;
 import static com.android.wallpaperbackup.WallpaperEventLogger.ERROR_NO_METADATA;
@@ -39,6 +40,7 @@
 import android.content.SharedPreferences;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
+import android.graphics.BitmapFactory;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
@@ -109,22 +111,16 @@
     static final String LOCK_WALLPAPER_STAGE = "wallpaper-lock-stage";
     @VisibleForTesting
     static final String WALLPAPER_INFO_STAGE = "wallpaper-info-stage";
-
     @VisibleForTesting
     static final String WALLPAPER_BACKUP_DEVICE_INFO_STAGE = "wallpaper-backup-device-info-stage";
-
     static final String EMPTY_SENTINEL = "empty";
     static final String QUOTA_SENTINEL = "quota";
-
     // Shared preferences constants.
     static final String PREFS_NAME = "wbprefs.xml";
     static final String SYSTEM_GENERATION = "system_gen";
     static final String LOCK_GENERATION = "lock_gen";
 
-    /**
-     * An approximate area threshold to compare device dimension similarity
-     */
-    static final int AREA_THRESHOLD = 50; // TODO (b/327637867): determine appropriate threshold
+    static final float DEFAULT_ACCEPTABLE_PARALLAX = 0.2f;
 
     // If this file exists, it means we exceeded our quota last time
     private File mQuotaFile;
@@ -336,7 +332,6 @@
         mEventLogger.onSystemImageWallpaperBackupFailed(error);
     }
 
-
     private void backupLockWallpaperFileIfItExists(SharedPreferences sharedPrefs,
             boolean lockChanged, int lockGeneration, FullBackupDataOutput data) throws IOException {
         final File lockImageStage = new File(getFilesDir(), LOCK_WALLPAPER_STAGE);
@@ -409,6 +404,16 @@
         }
     }
 
+    private static String readText(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        String result = "";
+        if (parser.next() == XmlPullParser.TEXT) {
+            result = parser.getText();
+            parser.nextTag();
+        }
+        return result;
+    }
+
     @VisibleForTesting
     // fullBackupFile is final, so we intercept backups here in tests.
     protected void backupFile(File file, FullBackupDataOutput data) {
@@ -438,18 +443,10 @@
         boolean lockImageStageExists = lockImageStage.exists();
 
         try {
-            // Parse the device dimensions of the source device and compare with target to
-            // to identify whether we need to skip the remainder of the restore process
+            // Parse the device dimensions of the source device
             Pair<Point, Point> sourceDeviceDimensions = parseDeviceDimensions(
                     deviceDimensionsStage);
 
-            Point targetDeviceDimensions = getScreenDimensions();
-            if (sourceDeviceDimensions != null && targetDeviceDimensions != null
-                    && isSourceDeviceSignificantlySmallerThanTarget(sourceDeviceDimensions.first,
-                    targetDeviceDimensions)) {
-                Slog.d(TAG, "The source device is significantly smaller than target");
-            }
-
             // First parse the live component name so that we know for logging if we care about
             // logging errors with the image restore.
             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
@@ -466,9 +463,10 @@
             // to back up the original image on the source device, or there was no user-supplied
             // wallpaper image present.
             if (lockImageStageExists) {
-                restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
+                restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK,
+                        sourceDeviceDimensions);
             }
-            restoreFromStage(imageStage, infoStage, "wp", sysWhich);
+            restoreFromStage(imageStage, infoStage, "wp", sysWhich, sourceDeviceDimensions);
 
             // And reset to the wallpaper service we should be using
             if (mLockHasLiveComponent) {
@@ -543,16 +541,6 @@
         }
     }
 
-    private static String readText(TypedXmlPullParser parser)
-            throws IOException, XmlPullParserException {
-        String result = "";
-        if (parser.next() == XmlPullParser.TEXT) {
-            result = parser.getText();
-            parser.nextTag();
-        }
-        return result;
-    }
-
     @VisibleForTesting
     void updateWallpaperComponent(ComponentName wpService, int which)
             throws IOException {
@@ -578,10 +566,13 @@
         }
     }
 
-    private void restoreFromStage(File stage, File info, String hintTag, int which)
+    private void restoreFromStage(File stage, File info, String hintTag, int which,
+            Pair<Point, Point> sourceDeviceDimensions)
             throws IOException {
         if (stage.exists()) {
             if (multiCrop()) {
+                // TODO(b/332937943): implement offset adjustment by manually adjusting crop to
+                //  adhere to device aspect ratio
                 SparseArray<Rect> cropHints = parseCropHints(info, hintTag);
                 if (cropHints != null) {
                     Slog.i(TAG, "Got restored wallpaper; applying which=" + which
@@ -601,7 +592,6 @@
                 }
                 return;
             }
-
             // Parse the restored info file to find the crop hint.  Note that this currently
             // relies on a priori knowledge of the wallpaper info file schema.
             Rect cropHint = parseCropHint(info, hintTag);
@@ -609,8 +599,33 @@
                 Slog.i(TAG, "Got restored wallpaper; applying which=" + which
                         + "; cropHint = " + cropHint);
                 try (FileInputStream in = new FileInputStream(stage)) {
-                    mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint, true,
-                            which);
+
+                    if (sourceDeviceDimensions != null && sourceDeviceDimensions.first != null) {
+                        BitmapFactory.Options options = new BitmapFactory.Options();
+                        options.inJustDecodeBounds = true;
+                        ParcelFileDescriptor pdf = ParcelFileDescriptor.open(stage, MODE_READ_ONLY);
+                        BitmapFactory.decodeFileDescriptor(pdf.getFileDescriptor(),
+                                null, options);
+                        Point bitmapSize = new Point(options.outWidth, options.outHeight);
+                        Point sourceDeviceSize = new Point(sourceDeviceDimensions.first.x,
+                                sourceDeviceDimensions.first.y);
+                        Point targetDeviceDimensions = getScreenDimensions();
+
+                        // TODO: for now we handle only the case where the target device has smaller
+                        // aspect ratio than the source device i.e. the target device is more narrow
+                        // than the source device
+                        if (isTargetMoreNarrowThanSource(targetDeviceDimensions,
+                                sourceDeviceSize)) {
+                            Rect adjustedCrop = findNewCropfromOldCrop(cropHint,
+                                    sourceDeviceDimensions.first, true, targetDeviceDimensions,
+                                    bitmapSize, true);
+
+                            cropHint.set(adjustedCrop);
+                        }
+                    }
+
+                    mWallpaperManager.setStream(in, cropHint.isEmpty() ? null : cropHint,
+                            true, which);
 
                     // And log the success
                     if ((which & FLAG_SYSTEM) > 0) {
@@ -629,6 +644,209 @@
         }
     }
 
+    /**
+     * This method computes the crop of the stored wallpaper to preserve its center point as the
+     * user had set it in the previous device.
+     *
+     * The algorithm involves first computing the original crop of the user (without parallax). Then
+     * manually adjusting the user's original crop to respect the current device's aspect ratio
+     * (thereby preserving the center point). Then finally, adding any leftover image real-estate
+     * (i.e. space left over on the horizontal axis) to add parallax effect. Parallax is only added
+     * if was present in the old device's settings.
+     *
+     */
+    private Rect findNewCropfromOldCrop(Rect oldCrop, Point oldDisplaySize, boolean oldRtl,
+            Point newDisplaySize, Point bitmapSize, boolean newRtl) {
+        Rect cropWithoutParallax = withoutParallax(oldCrop, oldDisplaySize, oldRtl, bitmapSize);
+        oldCrop = oldCrop.isEmpty() ? new Rect(0, 0, bitmapSize.x, bitmapSize.y) : oldCrop;
+        float oldParallaxAmount = ((float) oldCrop.width() / cropWithoutParallax.width()) - 1;
+
+        Rect newCropWithSameCenterWithoutParallax = sameCenter(newDisplaySize, bitmapSize,
+                cropWithoutParallax);
+
+        Rect newCrop = newCropWithSameCenterWithoutParallax;
+
+        // calculate the amount of left-over space there is in the image after adjusting the crop
+        // from the above operation i.e. in a rtl configuration, this is the remaining space in the
+        // image after subtracting the new crop's right edge coordinate from the image itself, and
+        // for ltr, its just the new crop's left edge coordinate (as it's the distance from the
+        // beginning of the image)
+        int widthAvailableForParallaxOnTheNewDevice =
+                (newRtl) ? newCrop.left : bitmapSize.x - newCrop.right;
+
+        // calculate relatively how much this available space is as a fraction of the total cropped
+        // image
+        float availableParallaxAmount =
+                (float) widthAvailableForParallaxOnTheNewDevice / newCrop.width();
+
+        float minAcceptableParallax = Math.min(DEFAULT_ACCEPTABLE_PARALLAX, oldParallaxAmount);
+
+        if (DEBUG) {
+            Slog.d(TAG, "- cropWithoutParallax: " + cropWithoutParallax);
+            Slog.d(TAG, "- oldParallaxAmount: " + oldParallaxAmount);
+            Slog.d(TAG, "- newCropWithSameCenterWithoutParallax: "
+                    + newCropWithSameCenterWithoutParallax);
+            Slog.d(TAG, "- widthAvailableForParallaxOnTheNewDevice: "
+                    + widthAvailableForParallaxOnTheNewDevice);
+            Slog.d(TAG, "- availableParallaxAmount: " + availableParallaxAmount);
+            Slog.d(TAG, "- minAcceptableParallax: " + minAcceptableParallax);
+            Slog.d(TAG, "- oldCrop: " + oldCrop);
+            Slog.d(TAG, "- oldDisplaySize: " + oldDisplaySize);
+            Slog.d(TAG, "- oldRtl: " + oldRtl);
+            Slog.d(TAG, "- newDisplaySize: " + newDisplaySize);
+            Slog.d(TAG, "- bitmapSize: " + bitmapSize);
+            Slog.d(TAG, "- newRtl: " + newRtl);
+        }
+        if (availableParallaxAmount >= minAcceptableParallax) {
+            // but in any case, don't put more parallax than the amount of the old device
+            float parallaxToAdd = Math.min(availableParallaxAmount, oldParallaxAmount);
+
+            int widthToAddForParallax = (int) (newCrop.width() * parallaxToAdd);
+            if (DEBUG) {
+                Slog.d(TAG, "- parallaxToAdd: " + parallaxToAdd);
+                Slog.d(TAG, "- widthToAddForParallax: " + widthToAddForParallax);
+            }
+            if (newRtl) {
+                newCrop.left -= widthToAddForParallax;
+            } else {
+                newCrop.right += widthToAddForParallax;
+            }
+        }
+        return newCrop;
+    }
+
+    /**
+     * This method computes the original crop of the user without parallax.
+     *
+     * NOTE: When the user sets the wallpaper with a specific crop, there may additional image added
+     * to the crop to support parallax. In order to determine the user's actual crop the parallax
+     * must be removed if it exists.
+     */
+    Rect withoutParallax(Rect crop, Point displaySize, boolean rtl, Point bitmapSize) {
+        // in the case an image's crop is not set, we assume the image itself is cropped
+        if (crop.isEmpty()) {
+            crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        }
+
+        if (DEBUG) {
+            Slog.w(TAG, "- crop: " + crop);
+        }
+
+        Rect adjustedCrop = new Rect(crop);
+        float suggestedDisplayRatio = (float) displaySize.x / displaySize.y;
+
+        // here we calculate the width of the wallpaper image such that it has the same aspect ratio
+        // as the given display i.e. the width of the image on a single page of the device without
+        // parallax (i.e. displaySize will correspond to the display the crop was originally set on)
+        int wallpaperWidthWithoutParallax = (int) (0.5f + (float) displaySize.x * crop.height()
+                / displaySize.y);
+        // subtracting wallpaperWidthWithoutParallax from the wallpaper crop gives the amount of
+        // parallax added
+        int widthToRemove = Math.max(0, crop.width() - wallpaperWidthWithoutParallax);
+
+        if (DEBUG) {
+            Slog.d(TAG, "- adjustedCrop: " + adjustedCrop);
+            Slog.d(TAG, "- suggestedDisplayRatio: " + suggestedDisplayRatio);
+            Slog.d(TAG, "- wallpaperWidthWithoutParallax: " + wallpaperWidthWithoutParallax);
+            Slog.d(TAG, "- widthToRemove: " + widthToRemove);
+        }
+        if (rtl) {
+            adjustedCrop.left += widthToRemove;
+        } else {
+            adjustedCrop.right -= widthToRemove;
+        }
+
+        if (DEBUG) {
+            Slog.d(TAG, "- adjustedCrop: " + crop);
+        }
+        return adjustedCrop;
+    }
+
+    /**
+     * This method computes a new crop based on the given crop in order to preserve the center point
+     * of the given crop on the provided displaySize. This is only for the case where the device
+     * displaySize has a smaller aspect ratio than the cropped image.
+     *
+     * NOTE: If the width to height ratio is less in the device display than cropped image
+     * this means the aspect ratios are off and there will be distortions in the image
+     * if the image is applied to the current display (i.e. the image will be skewed ->
+     * pixels in the image will not align correctly with the same pixels in the image that are
+     * above them)
+     */
+    Rect sameCenter(Point displaySize, Point bitmapSize, Rect crop) {
+
+        // in the case an image's crop is not set, we assume the image itself is cropped
+        if (crop.isEmpty()) {
+            crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
+        }
+
+        float screenRatio = (float) displaySize.x / displaySize.y;
+        float cropRatio = (float) crop.width() / crop.height();
+
+        Rect adjustedCrop = new Rect(crop);
+
+        if (screenRatio < cropRatio) {
+            // the screen is more narrow than the image, and as such, the image will need to be
+            // zoomed in till it fits in the vertical axis. Due to this, we need to manually adjust
+            // the image's crop in order for it to fit into the screen without having the framework
+            // do it (since the framework left aligns the image after zooming)
+
+            // Calculate the height of the adjusted wallpaper crop so it respects the aspect ratio
+            // of the device. To calculate the height, we will use the width of the current crop.
+            // This is so we find the largest height possible which also respects the device aspect
+            // ratio.
+            int heightToAdd = (int) (0.5f + crop.width() / screenRatio - crop.height());
+
+            // Calculate how much extra image space available that can be used to adjust
+            // the crop. If this amount is less than heightToAdd, from above, then that means we
+            // can't use heightToAdd. Instead we will need to use the maximum possible height, which
+            // is the height of the original bitmap. NOTE: the bitmap height may be different than
+            // the crop.
+            // since there is no guarantee to have height available on both sides
+            // (e.g. the available height might be fully at the bottom), grab the minimum
+            int availableHeight = 2 * Math.min(crop.top, bitmapSize.y - crop.bottom);
+            int actualHeightToAdd = Math.min(heightToAdd, availableHeight);
+
+            // half of the additional height is added to the top and bottom of the crop
+            adjustedCrop.top -= actualHeightToAdd / 2 + actualHeightToAdd % 2;
+            adjustedCrop.bottom += actualHeightToAdd / 2;
+
+            // Calculate the width of the adjusted crop. Initially we used the fixed width of the
+            // crop to calculate the heightToAdd, but since this height may be invalid (based on
+            // the calculation above) we calculate the width again instead of using the fixed width,
+            // using the adjustedCrop's updated height.
+            int widthToRemove = (int) (0.5f + crop.width() - adjustedCrop.height() * screenRatio);
+
+            // half of the additional width is subtracted from the left and right side of the crop
+            int widthToRemoveLeft = widthToRemove / 2;
+            int widthToRemoveRight = widthToRemove / 2 + widthToRemove % 2;
+
+            adjustedCrop.left += widthToRemoveLeft;
+            adjustedCrop.right -= widthToRemoveRight;
+
+            if (DEBUG) {
+                Slog.d(TAG, "cropRatio: " + cropRatio);
+                Slog.d(TAG, "screenRatio: " + screenRatio);
+                Slog.d(TAG, "heightToAdd: " + heightToAdd);
+                Slog.d(TAG, "actualHeightToAdd: " + actualHeightToAdd);
+                Slog.d(TAG, "availableHeight: " + availableHeight);
+                Slog.d(TAG, "widthToRemove: " + widthToRemove);
+                Slog.d(TAG, "adjustedCrop: " + adjustedCrop);
+            }
+
+            return adjustedCrop;
+        }
+
+        return adjustedCrop;
+    }
+
+    private boolean isTargetMoreNarrowThanSource(Point targetDisplaySize, Point srcDisplaySize) {
+        float targetScreenRatio = (float) targetDisplaySize.x / targetDisplaySize.y;
+        float srcScreenRatio = (float) srcDisplaySize.x / srcDisplaySize.y;
+
+        return (targetScreenRatio < srcScreenRatio);
+    }
+
     private void logRestoreErrorIfNoLiveComponent(int which, String error) {
         if (mSystemHasLiveComponent) {
             return;
@@ -644,6 +862,7 @@
             mEventLogger.onLockImageWallpaperRestoreFailed(error);
         }
     }
+
     private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
         Rect cropHint = new Rect();
         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
@@ -681,7 +900,7 @@
                 if (type != XmlPullParser.START_TAG) continue;
                 String tag = parser.getName();
                 if (!sectionTag.equals(tag)) continue;
-                for (Pair<Integer, String> pair: List.of(
+                for (Pair<Integer, String> pair : List.of(
                         new Pair<>(WallpaperManager.PORTRAIT, "Portrait"),
                         new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"),
                         new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"),
@@ -907,22 +1126,6 @@
         return internalDisplays;
     }
 
-    /**
-     * This method compares the source and target dimensions, and returns true if there is a
-     * significant difference in area between them and the source dimensions are smaller than the
-     * target dimensions.
-     *
-     * @param sourceDimensions is the dimensions of the source device
-     * @param targetDimensions is the dimensions of the target device
-     */
-    @VisibleForTesting
-    boolean isSourceDeviceSignificantlySmallerThanTarget(Point sourceDimensions,
-            Point targetDimensions) {
-        int rawAreaDelta = (targetDimensions.x * targetDimensions.y)
-                - (sourceDimensions.x * sourceDimensions.y);
-        return rawAreaDelta > AREA_THRESHOLD;
-    }
-
     @VisibleForTesting
     boolean isDeviceInRestore() {
         try {
diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
index ec9223c..3ecdf3f 100644
--- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
+++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
@@ -59,7 +59,6 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
@@ -841,26 +840,6 @@
         testParseCropHints(testMap);
     }
 
-    @Test
-    public void test_sourceDimensionsAreLargerThanTarget() {
-        // source device is larger than target, expecting to get false
-        Point sourceDimensions = new Point(2208, 1840);
-        Point targetDimensions = new Point(1080, 2092);
-        boolean isSourceSmaller = mWallpaperBackupAgent
-                .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions);
-        assertThat(isSourceSmaller).isEqualTo(false);
-    }
-
-    @Test
-    public void test_sourceDimensionsMuchSmallerThanTarget() {
-        // source device is smaller than target, expecting to get true
-        Point sourceDimensions = new Point(1080, 2092);
-        Point targetDimensions = new Point(2208, 1840);
-        boolean isSourceSmaller = mWallpaperBackupAgent
-                .isSourceDeviceSignificantlySmallerThanTarget(sourceDimensions, targetDimensions);
-        assertThat(isSourceSmaller).isEqualTo(true);
-    }
-
     private void testParseCropHints(Map<Integer, Rect> testMap) throws Exception {
         assumeTrue(multiCrop());
         mockRestoredStaticWallpaperFile(testMap);
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index 147f8d1..374abe0 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -2014,7 +2014,7 @@
             if (!mBgCurrentDrainHighThresholdByBgLocation) {
                 return false;
             }
-            if (mTracker.mContext.checkPermission(ACCESS_BACKGROUND_LOCATION,
+            if (mTracker.mInjector.checkPermission(ACCESS_BACKGROUND_LOCATION,
                     Process.INVALID_PID, uid) == PERMISSION_GRANTED) {
                 return true;
             }
diff --git a/services/core/java/com/android/server/am/AppPermissionTracker.java b/services/core/java/com/android/server/am/AppPermissionTracker.java
index c641b35..a47beae 100644
--- a/services/core/java/com/android/server/am/AppPermissionTracker.java
+++ b/services/core/java/com/android/server/am/AppPermissionTracker.java
@@ -293,7 +293,7 @@
                 mPermissionGranted = true;
                 return;
             }
-            mPermissionGranted = mContext.checkPermission(mPermission, Process.INVALID_PID, mUid)
+            mPermissionGranted = mInjector.checkPermission(mPermission, Process.INVALID_PID, mUid)
                     == PERMISSION_GRANTED;
         }
 
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index 88f6bc9..8b1300b 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -235,6 +235,7 @@
     private final HandlerThread mBgHandlerThread;
     private final BgHandler mBgHandler;
     private final HandlerExecutor mBgExecutor;
+    private final HandlerExecutor mExecutor;
 
     // No lock is needed, as it's immutable after initialization in constructor.
     private final ArrayList<BaseAppStateTracker> mAppStateTrackers = new ArrayList<>();
@@ -1489,6 +1490,7 @@
         mConstantsObserver = new ConstantsObserver(mBgHandler, mContext);
         mNotificationHelper = new NotificationHelper(this);
         injector.initAppStateTrackers(this);
+        mExecutor = new HandlerExecutor(injector.getDefaultHandler());
     }
 
     void onSystemReady() {
@@ -1506,7 +1508,7 @@
         mInjector.getAppStateTracker().addBackgroundRestrictedAppListener(
                 mBackgroundRestrictionListener);
         mInjector.getAppStandbyInternal().addListener(mAppIdleStateChangeListener);
-        mInjector.getRoleManager().addOnRoleHoldersChangedListenerAsUser(mBgExecutor,
+        mInjector.getRoleManager().addOnRoleHoldersChangedListenerAsUser(mExecutor,
                 mRoleHolderChangedListener, UserHandle.ALL);
         mInjector.scheduleInitTrackers(mBgHandler, () -> {
             for (int i = 0, size = mAppStateTrackers.size(); i < size; i++) {
@@ -2896,7 +2898,7 @@
         for (int i = 0; i < numPhones; i++) {
             final PhoneCarrierPrivilegesCallback callback = new PhoneCarrierPrivilegesCallback(i);
             callbacks.add(callback);
-            telephonyManager.registerCarrierPrivilegesCallback(i, mBgExecutor, callback);
+            telephonyManager.registerCarrierPrivilegesCallback(i, mExecutor, callback);
         }
         mCarrierPrivilegesCallbacks = callbacks;
     }
@@ -3288,6 +3290,10 @@
             return System.currentTimeMillis();
         }
 
+        Handler getDefaultHandler() {
+            return mAppRestrictionController.mActivityManagerService.mHandler;
+        }
+
         boolean isTest() {
             return false;
         }
diff --git a/services/core/java/com/android/server/am/BaseAppStateTracker.java b/services/core/java/com/android/server/am/BaseAppStateTracker.java
index 8d60910..5179d58 100644
--- a/services/core/java/com/android/server/am/BaseAppStateTracker.java
+++ b/services/core/java/com/android/server/am/BaseAppStateTracker.java
@@ -75,11 +75,11 @@
     static final int STATE_TYPE_INDEX_PERMISSION = 4;
 
     protected final AppRestrictionController mAppRestrictionController;
-    protected final Injector<T> mInjector;
     protected final Context mContext;
     protected final Handler mBgHandler;
     protected final Object mLock;
     protected final ArrayList<StateListener> mStateListeners = new ArrayList<>();
+    final Injector<T> mInjector;
 
     interface StateListener {
         void onStateChange(int uid, String packageName, boolean start, long now, int stateType);
@@ -292,6 +292,7 @@
         RoleManager mRoleManager;
         NotificationManagerInternal mNotificationManagerInternal;
         IAppOpsService mIAppOpsService;
+        Context mContext;
 
         void setPolicy(T policy) {
             mAppStatePolicy = policy;
@@ -316,6 +317,7 @@
                     NotificationManagerInternal.class);
             mIAppOpsService = IAppOpsService.Stub.asInterface(
                     ServiceManager.getService(Context.APP_OPS_SERVICE));
+            mContext = context;
 
             getPolicy().onSystemReady();
         }
@@ -390,5 +392,9 @@
         IAppOpsService getIAppOpsService() {
             return mIAppOpsService;
         }
+
+        int checkPermission(String perm, int pid, int uid) {
+            return mContext.checkPermission(perm, pid, uid);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index df84653..d435c24 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1535,8 +1535,9 @@
         sendLMsgNoDelay(MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState);
     }
 
-    /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState) {
-        sendLMsgNoDelay(MSG_L_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, deviceState);
+    /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) {
+        sendILMsgNoDelay(
+                MSG_IL_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, initSA ? 1 : 0, deviceState);
     }
 
     /*package*/ static final class CommunicationDeviceInfo {
@@ -2068,8 +2069,8 @@
                         }
                     } break;
 
-                case MSG_L_UPDATED_ADI_DEVICE_STATE:
-                    mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj);
+                case MSG_IL_UPDATED_ADI_DEVICE_STATE:
+                    mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj, msg.arg1 == 1);
                     break;
 
                 default:
@@ -2156,7 +2157,7 @@
     private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56;
     private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
     private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58;
-    private static final int MSG_L_UPDATED_ADI_DEVICE_STATE = 59;
+    private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59;
 
 
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 8b64d93..ecb08c20 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -169,7 +169,7 @@
             if (ads != null) {
                 if (ads.getAudioDeviceCategory() != category) {
                     ads.setAudioDeviceCategory(category);
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/);
                     mDeviceBroker.postPersistAudioDeviceSettings();
                 }
                 mDeviceBroker.postSynchronizeAdiDevicesInInventory(ads);
@@ -182,7 +182,7 @@
             mDeviceInventory.put(ads.getDeviceId(), ads);
             checkDeviceInventorySize_l();
 
-            mDeviceBroker.postUpdatedAdiDeviceState(ads);
+            mDeviceBroker.postUpdatedAdiDeviceState(ads, true /*initSA*/);
             mDeviceBroker.postPersistAudioDeviceSettings();
         }
     }
@@ -212,7 +212,7 @@
             checkDeviceInventorySize_l();
         }
         if (updatedCategory.get()) {
-            mDeviceBroker.postUpdatedAdiDeviceState(deviceState);
+            mDeviceBroker.postUpdatedAdiDeviceState(deviceState, false /*initSA*/);
         }
         mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState);
     }
@@ -314,7 +314,7 @@
                     }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads2);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/);
                     AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                             "synchronizeBleDeviceInInventory synced device pair ads1="
                                     + updatedDevice + " ads2=" + ads2).printLog(TAG));
@@ -335,7 +335,7 @@
                     }
                     ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-                    mDeviceBroker.postUpdatedAdiDeviceState(ads2);
+                    mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/);
                     AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                             "synchronizeBleDeviceInInventory synced device pair ads1="
                                     + updatedDevice + " peer ads2=" + ads2).printLog(TAG));
@@ -360,7 +360,7 @@
             }
             ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
 
-            mDeviceBroker.postUpdatedAdiDeviceState(ads);
+            mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/);
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                     "synchronizeDeviceProfilesInInventory synced device pair ads1="
                             + updatedDevice + " ads2=" + ads).printLog(TAG));
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d4f04b5..45076d04 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -11253,7 +11253,8 @@
         mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
         mDeviceBroker.postPersistAudioDeviceSettings();
 
-        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
+        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(),
+                false /* initState */);
         mSoundDoseHelper.setAudioDeviceCategory(addr, internalType,
                 btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES);
     }
@@ -11324,11 +11325,11 @@
 
     /** Update the sound dose and spatializer state based on the new AdiDeviceState. */
     @VisibleForTesting(visibility = PACKAGE)
-    public void onUpdatedAdiDeviceState(AdiDeviceState deviceState) {
+    public void onUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) {
         if (deviceState == null) {
             return;
         }
-        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
+        mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(), initSA);
         mSoundDoseHelper.setAudioDeviceCategory(deviceState.getDeviceAddress(),
                 deviceState.getInternalDeviceType(),
                 deviceState.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_HEADPHONES);
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 3b5fa7f..38fa79f 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -295,11 +295,11 @@
             // could have been called another time after boot in case of audioserver restart
             addCompatibleAudioDevice(
                     new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
-                            false /*forceEnable*/);
+                            false /*forceEnable*/, false /*forceInit*/);
             // not force-enabling as this device might already be in the device list
             addCompatibleAudioDevice(
                     new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
-                            false /*forceEnable*/);
+                            false /*forceEnable*/, false /*forceInit*/);
         } catch (RemoteException e) {
             resetCapabilities();
         } finally {
@@ -526,7 +526,7 @@
     }
 
     synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
-        addCompatibleAudioDevice(ada, true /*forceEnable*/);
+        addCompatibleAudioDevice(ada, true /*forceEnable*/, false /*forceInit*/);
     }
 
     /**
@@ -540,7 +540,7 @@
      */
     @GuardedBy("this")
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
-            boolean forceEnable) {
+            boolean forceEnable, boolean forceInit) {
         if (!isDeviceCompatibleWithSpatializationModes(ada)) {
             return;
         }
@@ -548,6 +548,9 @@
         final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada);
         AdiDeviceState updatedDevice = null; // non-null on update.
         if (deviceState != null) {
+            if (forceInit) {
+                initSAState(deviceState);
+            }
             if (forceEnable && !deviceState.isSAEnabled()) {
                 updatedDevice = deviceState;
                 updatedDevice.setSAEnabled(true);
@@ -756,10 +759,10 @@
         }
     }
 
-    synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada) {
+    synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada, boolean initState) {
         final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada);
         if (isAvailableForAdiDeviceState(deviceState)) {
-            addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled());
+            addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled(), initState);
             setHeadTrackerEnabled(deviceState.isHeadTrackerEnabled(), ada);
         } else {
             removeCompatibleAudioDevice(ada);
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 45cf10b..5aa0ed7 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -327,14 +327,14 @@
                 R.dimen.config_letterboxBackgroundWallpaperBlurRadius);
         mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat(
                 R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha);
-        mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat(
-                R.dimen.config_letterboxHorizontalPositionMultiplier);
-        mLetterboxVerticalPositionMultiplier = mContext.getResources().getFloat(
-                R.dimen.config_letterboxVerticalPositionMultiplier);
-        mLetterboxBookModePositionMultiplier = mContext.getResources().getFloat(
-                R.dimen.config_letterboxBookModePositionMultiplier);
-        mLetterboxTabletopModePositionMultiplier = mContext.getResources().getFloat(
-                R.dimen.config_letterboxTabletopModePositionMultiplier);
+        setLetterboxHorizontalPositionMultiplier(mContext.getResources().getFloat(
+                R.dimen.config_letterboxHorizontalPositionMultiplier));
+        setLetterboxVerticalPositionMultiplier(mContext.getResources().getFloat(
+                R.dimen.config_letterboxVerticalPositionMultiplier));
+        setLetterboxBookModePositionMultiplier(mContext.getResources().getFloat(
+                R.dimen.config_letterboxBookModePositionMultiplier));
+        setLetterboxTabletopModePositionMultiplier(mContext.getResources()
+                .getFloat(R.dimen.config_letterboxTabletopModePositionMultiplier));
         mIsHorizontalReachabilityEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsHorizontalReachabilityEnabled);
         mIsVerticalReachabilityEnabled = mContext.getResources().getBoolean(
@@ -657,29 +657,8 @@
      * right side.
      */
     float getLetterboxHorizontalPositionMultiplier(boolean isInBookMode) {
-        if (isInBookMode) {
-            if (mLetterboxBookModePositionMultiplier < 0.0f
-                    || mLetterboxBookModePositionMultiplier > 1.0f) {
-                Slog.w(TAG,
-                        "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=true): "
-                        + mLetterboxBookModePositionMultiplier);
-                // Default to left position if invalid value is provided.
-                return 0.0f;
-            } else {
-                return mLetterboxBookModePositionMultiplier;
-            }
-        } else {
-            if (mLetterboxHorizontalPositionMultiplier < 0.0f
-                    || mLetterboxHorizontalPositionMultiplier > 1.0f) {
-                Slog.w(TAG,
-                        "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=false):"
-                        + mLetterboxBookModePositionMultiplier);
-                // Default to central position if invalid value is provided.
-                return 0.5f;
-            } else {
-                return mLetterboxHorizontalPositionMultiplier;
-            }
-        }
+        return isInBookMode ? mLetterboxBookModePositionMultiplier
+                : mLetterboxHorizontalPositionMultiplier;
     }
 
     /*
@@ -689,37 +668,28 @@
      * bottom side.
      */
     float getLetterboxVerticalPositionMultiplier(boolean isInTabletopMode) {
-        if (isInTabletopMode) {
-            return (mLetterboxTabletopModePositionMultiplier < 0.0f
-                    || mLetterboxTabletopModePositionMultiplier > 1.0f)
-                    // Default to top position if invalid value is provided.
-                    ? 0.0f : mLetterboxTabletopModePositionMultiplier;
-        } else {
-            return (mLetterboxVerticalPositionMultiplier < 0.0f
-                    || mLetterboxVerticalPositionMultiplier > 1.0f)
-                    // Default to central position if invalid value is provided.
-                    ? 0.5f : mLetterboxVerticalPositionMultiplier;
-        }
+        return isInTabletopMode ? mLetterboxTabletopModePositionMultiplier
+                : mLetterboxVerticalPositionMultiplier;
     }
 
     /**
-     * Overrides horizontal position of a center of the letterboxed app window. If given value < 0
-     * or > 1, then it and a value of {@link
-     * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} are ignored and
-     * central position (0.5) is used.
+     * Overrides horizontal position of a center of the letterboxed app window.
+     *
+     * @throws IllegalArgumentException If given value < 0 or > 1.
      */
     void setLetterboxHorizontalPositionMultiplier(float multiplier) {
-        mLetterboxHorizontalPositionMultiplier = multiplier;
+        mLetterboxHorizontalPositionMultiplier = assertValidMultiplier(multiplier,
+                "mLetterboxHorizontalPositionMultiplier");
     }
 
     /**
-     * Overrides vertical position of a center of the letterboxed app window. If given value < 0
-     * or > 1, then it and a value of {@link
-     * com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier} are ignored and
-     * central position (0.5) is used.
+     * Overrides vertical position of a center of the letterboxed app window.
+     *
+     * @throws IllegalArgumentException If given value < 0 or > 1.
      */
     void setLetterboxVerticalPositionMultiplier(float multiplier) {
-        mLetterboxVerticalPositionMultiplier = multiplier;
+        mLetterboxVerticalPositionMultiplier = assertValidMultiplier(multiplier,
+                "mLetterboxVerticalPositionMultiplier");
     }
 
     /**
@@ -740,6 +710,28 @@
                 com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier);
     }
 
+    /**
+     * Sets tabletop mode position multiplier.
+     *
+     * @throws IllegalArgumentException If given value < 0 or > 1.
+     */
+    @VisibleForTesting
+    void setLetterboxTabletopModePositionMultiplier(float multiplier) {
+        mLetterboxTabletopModePositionMultiplier = assertValidMultiplier(multiplier,
+                "mLetterboxTabletopModePositionMultiplier");
+    }
+
+    /**
+     * Sets tabletop mode position multiplier.
+     *
+     * @throws IllegalArgumentException If given value < 0 or > 1.
+     */
+    @VisibleForTesting
+    void setLetterboxBookModePositionMultiplier(float multiplier) {
+        mLetterboxBookModePositionMultiplier = assertValidMultiplier(multiplier,
+                "mLetterboxBookModePositionMultiplier");
+    }
+
     /*
      * Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps in
      * landscape device orientation.
@@ -1356,4 +1348,21 @@
     void resetUserAppAspectRatioFullscreenEnabled() {
         setUserAppAspectRatioFullscreenOverrideEnabled(false);
     }
+
+    /**
+     * Checks whether the multiplier is between [0,1].
+     *
+     * @param multiplierName sent in the exception if multiplier is invalid, for easier debugging.
+     *
+     * @return multiplier, if valid
+     * @throws IllegalArgumentException if outside bounds.
+     */
+    private float assertValidMultiplier(float multiplier, String multiplierName)
+            throws IllegalArgumentException {
+        if (multiplier < 0.0f || multiplier > 1.0f) {
+            throw new IllegalArgumentException("Trying to set " + multiplierName
+                    + " out of bounds: " + multiplier);
+        }
+        return multiplier;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index a6db310f..85b4d59 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -842,7 +842,12 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
+            try {
+                mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
+            } catch (IllegalArgumentException  e) {
+                getErrPrintWriter().println("Error: invalid multiplier value " + e);
+                return -1;
+            }
         }
         return 0;
     }
@@ -861,7 +866,12 @@
             return -1;
         }
         synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier);
+            try {
+                mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier);
+            } catch (IllegalArgumentException  e) {
+                getErrPrintWriter().println("Error: invalid multiplier value " + e);
+                return -1;
+            }
         }
         return 0;
     }
@@ -1532,9 +1542,9 @@
         pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
         pw.println("        be ignored and framework implementation will determine aspect ratio.");
         pw.println("      --cornerRadius radius");
-        pw.println("        Corners radius for activities in the letterbox mode. If radius < 0,");
-        pw.println("        both it and R.integer.config_letterboxActivityCornersRadius will be");
-        pw.println("        ignored and corners of the activity won't be rounded.");
+        pw.println("        Corners radius (in pixels) for activities in the letterbox mode.");
+        pw.println("        If radius < 0, both R.integer.config_letterboxActivityCornersRadius");
+        pw.println("        and it will be ignored and corners of the activity won't be rounded.");
         pw.println("      --backgroundType [reset|solid_color|app_color_background");
         pw.println("          |app_color_background_floating|wallpaper]");
         pw.println("        Type of background used in the letterbox mode.");
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
index 067dd3b..637c73f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -126,6 +126,7 @@
 import android.os.BatteryStatsInternal;
 import android.os.BatteryUsageStats;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.MessageQueue;
 import android.os.Process;
 import android.os.RemoteException;
@@ -321,6 +322,7 @@
     private BindServiceEventListener mBindServiceEventListener;
 
     private Context mContext = getInstrumentation().getTargetContext();
+    private Handler mDefaultHandler = new Handler(Looper.getMainLooper());
     private TestBgRestrictionInjector mInjector;
     private AppRestrictionController mBgRestrictionController;
     private AppBatteryTracker mAppBatteryTracker;
@@ -346,10 +348,6 @@
         mActivityManagerService.mConstants = mActivityManagerConstants;
         mPhoneCarrierPrivileges = new PhoneCarrierPrivileges(
                 mInjector.getTelephonyManager(), MOCK_PRIVILEGED_PACKAGES.length);
-        for (int i = 0; i < MOCK_PRIVILEGED_PACKAGES.length; i++) {
-            mPhoneCarrierPrivileges.addNewPrivilegePackages(i,
-                    MOCK_PRIVILEGED_PACKAGES[i], MOCK_PRIVILEGED_UIDS[i]);
-        }
 
         doReturn(PROCESS_STATE_FOREGROUND_SERVICE).when(mActivityManagerInternal)
                 .getUidProcessState(anyInt());
@@ -3048,6 +3046,11 @@
 
     @Test
     public void testCarrierPrivilegedAppListener() throws Exception {
+        for (int i = 0; i < MOCK_PRIVILEGED_PACKAGES.length; i++) {
+            mPhoneCarrierPrivileges.addNewPrivilegePackages(i,
+                    MOCK_PRIVILEGED_PACKAGES[i], MOCK_PRIVILEGED_UIDS[i]);
+        }
+
         final long shortMs = 1_000L;
         for (int i = 0; i < MOCK_PRIVILEGED_PACKAGES.length; i++) {
             verifyPotentialSystemExemptionReason(REASON_CARRIER_PRIVILEGED_APP,
@@ -3356,6 +3359,11 @@
         }
 
         @Override
+        Handler getDefaultHandler() {
+            return mDefaultHandler;
+        }
+
+        @Override
         boolean isTest() {
             return true;
         }
@@ -3445,6 +3453,16 @@
         IAppOpsService getIAppOpsService() {
             return BackgroundRestrictionTest.this.mIAppOpsService;
         }
+
+        @Override
+        int checkPermission(String perm, int pid, int uid) {
+            try {
+                return BackgroundRestrictionTest.this.mIActivityManager.checkPermission(
+                        perm, pid, uid);
+            } catch (RemoteException e) {
+                return PERMISSION_DENIED;
+            }
+        }
     }
 
     private class TestAppBatteryTrackerInjector extends TestBaseTrackerInjector<AppBatteryPolicy> {
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index f1c1dc3..59f4d56b 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
@@ -315,7 +316,7 @@
                                 mFakeBtDevice.getAddress()));
                 verify(mMockAudioService,
                         timeout(MAX_MESSAGE_HANDLING_DELAY_MS).times(0)).onUpdatedAdiDeviceState(
-                        eq(devState));
+                        eq(devState), anyBoolean());
             }
 
             // metadata set
@@ -326,7 +327,7 @@
                                 mFakeBtDevice.getAddress()));
                 verify(mMockAudioService,
                         timeout(MAX_MESSAGE_HANDLING_DELAY_MS)).onUpdatedAdiDeviceState(
-                        any());
+                        any(), anyBoolean());
             }
         } finally {
             // reset the metadata device type
@@ -354,7 +355,7 @@
         verify(mMockAudioService,
                 timeout(MAX_MESSAGE_HANDLING_DELAY_MS).atLeast(1)).onUpdatedAdiDeviceState(
                 ArgumentMatchers.argThat(devState -> devState.getAudioDeviceCategory()
-                        == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER));
+                        == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER), anyBoolean());
     }
 
     private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index 80e169d..b90fa21 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -25,6 +25,8 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
 
+import static com.android.server.wm.testing.Assert.assertThrows;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -37,6 +39,8 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.wm.testing.Assert;
+
 import org.junit.Before;
 import org.junit.Test;
 
@@ -288,4 +292,56 @@
                 false /* forTabletopMode */,
                 LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
     }
+
+    @Test
+    public void test_setLetterboxHorizontalPositionMultiplier_validValues() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(-1));
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(2));
+
+        // Does not throw an exception for values [0,1].
+        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0);
+        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
+        mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(1);
+    }
+
+    @Test
+    public void test_setLetterboxVerticalPositionMultiplier_validValues() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(-1));
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(2));
+
+        // Does not throw an exception for values [0,1].
+        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0);
+        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1);
+    }
+
+    @Test
+    public void test_setLetterboxBookModePositionMultiplier_validValues() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(-1));
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(2));
+
+        // Does not throw an exception for values [0,1].
+        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0);
+        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0.5f);
+        mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(1);
+    }
+
+    @Test
+    public void test_setLetterboxTabletopModePositionMultiplier_validValues() {
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(-1));
+        assertThrows(IllegalArgumentException.class,
+                () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(2));
+
+        // Does not throw an exception for values [0,1].
+        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0);
+        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f);
+        mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(1);
+    }
 }
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 faa36a8..dbc54b2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4101,31 +4101,6 @@
     }
 
     @Test
-    public void testUpdateResolvedBoundsHorizontalPosition_invalidMultiplier_defaultToCenter() {
-        // Display configured as (2800, 1400).
-
-        // Below 0.0.
-        assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity(
-                /* letterboxHorizontalPositionMultiplier */ -1.0f,
-                // At launch.
-                /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400),
-                // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400),
-                // After the display is resized to (700, 1400).
-                /* sizeCompatScaled */ new Rect(525, 0, 875, 700));
-
-        // Above 1.0
-        assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity(
-                /* letterboxHorizontalPositionMultiplier */ 2.0f,
-                // At launch.
-                /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400),
-                // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400),
-                // After the display is resized to (700, 1400).
-                /* sizeCompatScaled */ new Rect(525, 0, 875, 700));
-    }
-
-    @Test
     public void testUpdateResolvedBoundsHorizontalPosition_right() {
         // Display configured as (2800, 1400).
         assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity(
@@ -4331,31 +4306,6 @@
     }
 
     @Test
-    public void testUpdateResolvedBoundsVerticalPosition_invalidMultiplier_defaultToCenter() {
-        // Display configured as (1400, 2800).
-
-        // Below 0.0.
-        assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
-                /* letterboxVerticalPositionMultiplier */ -1.0f,
-                // At launch.
-                /* fixedOrientationLetterbox */ new Rect(0, 1050, 1400, 1750),
-                // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(700, 350, 2100, 1050),
-                // After the display is resized to (1400, 700).
-                /* sizeCompatScaled */ new Rect(0, 525, 700, 875));
-
-        // Above 1.0
-        assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(
-                /* letterboxVerticalPositionMultiplier */ 2.0f,
-                // At launch.
-                /* fixedOrientationLetterbox */ new Rect(0, 1050, 1400, 1750),
-                // After 90 degree rotation.
-                /* sizeCompatUnscaled */ new Rect(700, 350, 2100, 1050),
-                // After the display is resized to (1400, 700).
-                /* sizeCompatScaled */ new Rect(0, 525, 700, 875));
-    }
-
-    @Test
     public void testUpdateResolvedBoundsVerticalPosition_bottom() {
         // Display configured as (1400, 2800).
         assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity(