Convert (new) ScreenshotController to kotlin

Bug: 354711957
Test: manual
Flag: com.android.systemui.screenshot_ui_controller_refactor
Change-Id: Ied1d12a672737315863f40f883ee0280c7d7dded
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index f69b0cb..7724abd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -505,8 +505,8 @@
             return;
         }
         // delay starting scroll capture to make sure scrim is up before the app moves
-        mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
-                mScreenshotTakenInPortrait, () -> executeBatchScrollCapture(response, owner));
+        mViewProxy.prepareScrollingTransition(response, newScreenshot, mScreenshotTakenInPortrait,
+                () -> executeBatchScrollCapture(response, owner));
     }
 
     private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
deleted file mode 100644
index fe58bc9..0000000
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ /dev/null
@@ -1,663 +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 static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-
-import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
-import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
-import static com.android.systemui.screenshot.LogConfig.logTag;
-import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
-import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-import android.view.ScrollCaptureResponse;
-import android.view.ViewRootImpl;
-import android.view.WindowManager;
-import android.widget.Toast;
-import android.window.WindowContext;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.settingslib.applications.InterestingConfigChanges;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.broadcast.BroadcastSender;
-import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.res.R;
-import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
-import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor;
-import com.android.systemui.util.Assert;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import dagger.assisted.Assisted;
-import dagger.assisted.AssistedFactory;
-import dagger.assisted.AssistedInject;
-
-import kotlin.Unit;
-
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.function.Consumer;
-
-import javax.inject.Provider;
-
-/**
- * Controls the state and flow for screenshots.
- */
-public class ScreenshotController implements InteractiveScreenshotHandler {
-    private static final String TAG = logTag(ScreenshotController.class);
-
-    // From WizardManagerHelper.java
-    private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
-
-    static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
-
-    private final WindowContext mContext;
-    private final FeatureFlags mFlags;
-    private final ScreenshotShelfViewProxy mViewProxy;
-    private final ScreenshotNotificationsController mNotificationsController;
-    private final ScreenshotSmartActions mScreenshotSmartActions;
-    private final UiEventLogger mUiEventLogger;
-    private final ImageExporter mImageExporter;
-    private final ImageCapture mImageCapture;
-    private final Executor mMainExecutor;
-    private final ExecutorService mBgExecutor;
-    private final BroadcastSender mBroadcastSender;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final ScreenshotActionsController mActionsController;
-
-    @Nullable
-    private final ScreenshotSoundController mScreenshotSoundController;
-    private final ScreenshotWindow mWindow;
-    private final Display mDisplay;
-    private final ScrollCaptureExecutor mScrollCaptureExecutor;
-    private final ScreenshotNotificationSmartActionsProvider
-            mScreenshotNotificationSmartActionsProvider;
-    private final TimeoutHandler mScreenshotHandler;
-    private final UserManager mUserManager;
-    private final AssistContentRequester mAssistContentRequester;
-    private final ActionExecutor mActionExecutor;
-
-
-    private final MessageContainerController mMessageContainerController;
-    private final AnnouncementResolver mAnnouncementResolver;
-    private Bitmap mScreenBitmap;
-    private boolean mScreenshotTakenInPortrait;
-    private Animator mScreenshotAnimation;
-    private RequestCallback mCurrentRequestCallback;
-    private String mPackageName = "";
-    private final BroadcastReceiver mCopyBroadcastReceiver;
-
-    /** Tracks config changes that require re-creating UI */
-    private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
-            ActivityInfo.CONFIG_ORIENTATION
-                    | ActivityInfo.CONFIG_LAYOUT_DIRECTION
-                    | ActivityInfo.CONFIG_LOCALE
-                    | ActivityInfo.CONFIG_UI_MODE
-                    | ActivityInfo.CONFIG_SCREEN_LAYOUT
-                    | ActivityInfo.CONFIG_ASSETS_PATHS);
-
-
-    @AssistedInject
-    ScreenshotController(
-            Context context,
-            ScreenshotWindow.Factory screenshotWindowFactory,
-            FeatureFlags flags,
-            ScreenshotShelfViewProxy.Factory viewProxyFactory,
-            ScreenshotSmartActions screenshotSmartActions,
-            ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
-            UiEventLogger uiEventLogger,
-            ImageExporter imageExporter,
-            ImageCapture imageCapture,
-            @Main Executor mainExecutor,
-            ScrollCaptureExecutor scrollCaptureExecutor,
-            TimeoutHandler timeoutHandler,
-            BroadcastSender broadcastSender,
-            BroadcastDispatcher broadcastDispatcher,
-            ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
-            ScreenshotActionsController.Factory screenshotActionsControllerFactory,
-            ActionExecutor.Factory actionExecutorFactory,
-            UserManager userManager,
-            AssistContentRequester assistContentRequester,
-            MessageContainerController messageContainerController,
-            Provider<ScreenshotSoundController> screenshotSoundController,
-            AnnouncementResolver announcementResolver,
-            @Assisted Display display
-    ) {
-        mScreenshotSmartActions = screenshotSmartActions;
-        mNotificationsController = screenshotNotificationsControllerFactory.create(
-                display.getDisplayId());
-        mUiEventLogger = uiEventLogger;
-        mImageExporter = imageExporter;
-        mImageCapture = imageCapture;
-        mMainExecutor = mainExecutor;
-        mScrollCaptureExecutor = scrollCaptureExecutor;
-        mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider;
-        mBgExecutor = Executors.newSingleThreadExecutor();
-        mBroadcastSender = broadcastSender;
-        mBroadcastDispatcher = broadcastDispatcher;
-
-        mScreenshotHandler = timeoutHandler;
-        mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
-
-        mDisplay = display;
-        mWindow = screenshotWindowFactory.create(mDisplay);
-        mContext = mWindow.getContext();
-        mFlags = flags;
-        mUserManager = userManager;
-        mMessageContainerController = messageContainerController;
-        mAssistContentRequester = assistContentRequester;
-        mAnnouncementResolver = announcementResolver;
-
-        mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId());
-
-        mScreenshotHandler.setOnTimeoutRunnable(() -> {
-            if (DEBUG_UI) {
-                Log.d(TAG, "Corner timeout hit");
-            }
-            mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT);
-        });
-
-        mConfigChanges.applyNewConfig(context.getResources());
-        reloadAssets();
-
-        mActionExecutor = actionExecutorFactory.create(mWindow.getWindow(), mViewProxy,
-                () -> {
-                    finishDismiss();
-                    return Unit.INSTANCE;
-                });
-        mActionsController = screenshotActionsControllerFactory.getController(mActionExecutor);
-
-
-        // Sound is only reproduced from the controller of the default display.
-        if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) {
-            mScreenshotSoundController = screenshotSoundController.get();
-        } else {
-            mScreenshotSoundController = null;
-        }
-
-        mCopyBroadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) {
-                    mViewProxy.requestDismissal(SCREENSHOT_DISMISSED_OTHER);
-                }
-            }
-        };
-        mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
-                        ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null,
-                Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION);
-    }
-
-    @Override
-    public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
-            RequestCallback requestCallback) {
-        Assert.isMainThread();
-
-        mCurrentRequestCallback = requestCallback;
-        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN
-                && screenshot.getBitmap() == null) {
-            Rect bounds = getFullScreenRect();
-            screenshot.setBitmap(mImageCapture.captureDisplay(mDisplay.getDisplayId(), bounds));
-            screenshot.setScreenBounds(bounds);
-        }
-
-        if (screenshot.getBitmap() == null) {
-            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null");
-            mNotificationsController.notifyScreenshotError(
-                    R.string.screenshot_failed_to_capture_text);
-            if (mCurrentRequestCallback != null) {
-                mCurrentRequestCallback.reportError();
-            }
-            return;
-        }
-
-        mScreenBitmap = screenshot.getBitmap();
-        String oldPackageName = mPackageName;
-        mPackageName = screenshot.getPackageNameString();
-
-        if (!isUserSetupComplete(Process.myUserHandle())) {
-            Log.w(TAG, "User setup not complete, displaying toast only");
-            // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
-            // and sharing shouldn't be exposed to the user.
-            saveScreenshotAndToast(screenshot, finisher);
-            return;
-        }
-
-        mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
-                ClipboardOverlayController.SELF_PERMISSION);
-
-        mScreenshotTakenInPortrait =
-                mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
-
-        // Optimizations
-        mScreenBitmap.setHasAlpha(false);
-        mScreenBitmap.prepareToDraw();
-
-        prepareViewForNewScreenshot(screenshot, oldPackageName);
-
-        final UUID requestId;
-        requestId = mActionsController.setCurrentScreenshot(screenshot);
-        saveScreenshotInBackground(screenshot, requestId, finisher, result -> {
-            if (result.uri != null) {
-                ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult(
-                        result.uri, screenshot.getUserOrDefault(), result.timestamp);
-                mActionsController.setCompletedScreenshot(requestId, savedScreenshot);
-            }
-        });
-
-        if (screenshot.getTaskId() >= 0) {
-            mAssistContentRequester.requestAssistContent(
-                    screenshot.getTaskId(),
-                    assistContent ->
-                            mActionsController.onAssistContent(requestId, assistContent));
-        } else {
-            mActionsController.onAssistContent(requestId, null);
-        }
-
-        // The window is focusable by default
-        mWindow.setFocusable(true);
-        mViewProxy.requestFocus();
-
-        enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle());
-
-        mWindow.attachWindow();
-
-        boolean showFlash;
-        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
-            if (screenshot.getScreenBounds() != null
-                    && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(),
-                    screenshot.getScreenBounds())) {
-                showFlash = false;
-            } else {
-                showFlash = true;
-                screenshot.setInsets(Insets.NONE);
-                screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(),
-                        screenshot.getBitmap().getHeight()));
-            }
-        } else {
-            showFlash = true;
-        }
-
-        mViewProxy.prepareEntranceAnimation(
-                () -> startAnimation(screenshot.getScreenBounds(), showFlash,
-                        () -> mMessageContainerController.onScreenshotTaken(screenshot)));
-
-        mViewProxy.setScreenshot(screenshot);
-
-    }
-
-    void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
-        mWindow.whenWindowAttached(() -> {
-            mAnnouncementResolver.getScreenshotAnnouncement(
-                    screenshot.getUserHandle().getIdentifier(),
-                    announcement -> {
-                        mViewProxy.announceForAccessibility(announcement);
-                    });
-        });
-
-        mViewProxy.reset();
-
-        if (mViewProxy.isAttachedToWindow()) {
-            // if we didn't already dismiss for another reason
-            if (!mViewProxy.isDismissing()) {
-                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0,
-                        oldPackageName);
-            }
-            if (DEBUG_WINDOW) {
-                Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
-                        + "(dismissing=" + mViewProxy.isDismissing() + ")");
-            }
-        }
-
-        mViewProxy.setPackageName(mPackageName);
-    }
-
-    /**
-     * Requests the view to dismiss the current screenshot (may be ignored, if screenshot is already
-     * being dismissed)
-     */
-    @Override
-    public void requestDismissal(ScreenshotEvent event) {
-        mViewProxy.requestDismissal(event);
-    }
-
-    @Override
-    public boolean isPendingSharedTransition() {
-        return mActionExecutor.isPendingSharedTransition();
-    }
-
-    // Any cleanup needed when the service is being destroyed.
-    @Override
-    public void onDestroy() {
-        removeWindow();
-        releaseMediaPlayer();
-        releaseContext();
-        mBgExecutor.shutdown();
-    }
-
-    /**
-     * Release the constructed window context.
-     */
-    private void releaseContext() {
-        mBroadcastDispatcher.unregisterReceiver(mCopyBroadcastReceiver);
-        mContext.release();
-    }
-
-    private void releaseMediaPlayer() {
-        if (mScreenshotSoundController == null) return;
-        mScreenshotSoundController.releaseScreenshotSoundAsync();
-    }
-
-    /**
-     * Update resources on configuration change. Reinflate for theme/color changes.
-     */
-    private void reloadAssets() {
-        if (DEBUG_UI) {
-            Log.d(TAG, "reloadAssets()");
-        }
-
-        mMessageContainerController.setView(mViewProxy.getView());
-        mViewProxy.setCallbacks(new ScreenshotShelfViewProxy.ScreenshotViewCallback() {
-            @Override
-            public void onUserInteraction() {
-                if (DEBUG_INPUT) {
-                    Log.d(TAG, "onUserInteraction");
-                }
-                mScreenshotHandler.resetTimeout();
-            }
-
-            @Override
-            public void onDismiss() {
-                finishDismiss();
-            }
-
-            @Override
-            public void onTouchOutside() {
-                // TODO(159460485): Remove this when focus is handled properly in the system
-                mWindow.setFocusable(false);
-            }
-        });
-
-        if (DEBUG_WINDOW) {
-            Log.d(TAG, "setContentView: " + mViewProxy.getView());
-        }
-        mWindow.setContentView(mViewProxy.getView());
-    }
-
-    private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) {
-        // Wait until this window is attached to request because it is
-        // the reference used to locate the target window (below).
-        mWindow.whenWindowAttached(() -> {
-            requestScrollCapture(requestId, owner);
-            mWindow.setActivityConfigCallback(
-                    new ViewRootImpl.ActivityConfigCallback() {
-                        @Override
-                        public void onConfigurationChanged(Configuration overrideConfig,
-                                int newDisplayId) {
-                            if (mConfigChanges.applyNewConfig(mContext.getResources())) {
-                                // Hide the scroll chip until we know it's available in this
-                                // orientation
-                                mActionsController.onScrollChipInvalidated();
-                                // Delay scroll capture eval a bit to allow the underlying activity
-                                // to set up in the new orientation.
-                                mScreenshotHandler.postDelayed(
-                                        () -> requestScrollCapture(requestId, owner), 150);
-                                mViewProxy.updateInsets(mWindow.getWindowInsets());
-                                // Screenshot animation calculations won't be valid anymore,
-                                // so just end
-                                if (mScreenshotAnimation != null
-                                        && mScreenshotAnimation.isRunning()) {
-                                    mScreenshotAnimation.end();
-                                }
-                            }
-                        }
-                    });
-        });
-    }
-
-    private void requestScrollCapture(UUID requestId, UserHandle owner) {
-        mScrollCaptureExecutor.requestScrollCapture(
-                mDisplay.getDisplayId(),
-                mWindow.getWindowToken(),
-                (response) -> {
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
-                            0, response.getPackageName());
-                    mActionsController.onScrollChipReady(requestId,
-                            () -> onScrollButtonClicked(owner, response));
-                    return Unit.INSTANCE;
-                }
-        );
-    }
-
-    private void onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response) {
-        if (DEBUG_INPUT) {
-            Log.d(TAG, "scroll chip tapped");
-        }
-        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
-                response.getPackageName());
-        Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(),
-                getFullScreenRect());
-        if (newScreenshot == null) {
-            Log.e(TAG, "Failed to capture current screenshot for scroll transition!");
-            return;
-        }
-        // delay starting scroll capture to make sure scrim is up before the app moves
-        mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
-                mScreenshotTakenInPortrait, () -> executeBatchScrollCapture(response, owner));
-    }
-
-    private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) {
-        mScrollCaptureExecutor.executeBatchScrollCapture(response,
-                () -> {
-                    final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent(
-                            owner, mContext);
-                    mContext.startActivity(intent);
-                },
-                mViewProxy::restoreNonScrollingUi,
-                mViewProxy::startLongScreenshotTransition);
-    }
-
-    @Override
-    public void removeWindow() {
-        mWindow.removeWindow();
-        mViewProxy.stopInputListening();
-    }
-
-    private void playCameraSoundIfNeeded() {
-        if (mScreenshotSoundController == null) return;
-        // the controller is not-null only on the default display controller
-        mScreenshotSoundController.playScreenshotSoundAsync();
-    }
-
-    /**
-     * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
-     * failure).
-     */
-    private void saveScreenshotAndToast(ScreenshotData screenshot, Consumer<Uri> finisher) {
-        // Play the shutter sound to notify that we've taken a screenshot
-        playCameraSoundIfNeeded();
-
-        saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> {
-            if (result.uri != null) {
-                mScreenshotHandler.post(() -> Toast.makeText(mContext,
-                        R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
-            }
-        });
-    }
-
-    /**
-     * Starts the animation after taking the screenshot
-     */
-    private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) {
-        if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
-            mScreenshotAnimation.cancel();
-        }
-
-        mScreenshotAnimation =
-                mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash);
-        if (onAnimationComplete != null) {
-            mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    super.onAnimationEnd(animation);
-                    onAnimationComplete.run();
-                }
-            });
-        }
-
-        // Play the shutter sound to notify that we've taken a screenshot
-        playCameraSoundIfNeeded();
-
-        if (DEBUG_ANIM) {
-            Log.d(TAG, "starting post-screenshot animation");
-        }
-        mScreenshotAnimation.start();
-    }
-
-    /** Reset screenshot view and then call onCompleteRunnable */
-    private void finishDismiss() {
-        Log.d(TAG, "finishDismiss");
-        mActionsController.endScreenshotSession();
-        mScrollCaptureExecutor.close();
-        if (mCurrentRequestCallback != null) {
-            mCurrentRequestCallback.onFinish();
-            mCurrentRequestCallback = null;
-        }
-        mViewProxy.reset();
-        removeWindow();
-        mScreenshotHandler.cancelTimeout();
-    }
-
-    private void saveScreenshotInBackground(ScreenshotData screenshot, UUID requestId,
-            Consumer<Uri> finisher, Consumer<ImageExporter.Result> onResult) {
-        ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
-                requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(),
-                mDisplay.getDisplayId());
-        future.addListener(() -> {
-            try {
-                ImageExporter.Result result = future.get();
-                Log.d(TAG, "Saved screenshot: " + result);
-                logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
-                onResult.accept(result);
-                if (DEBUG_CALLBACK) {
-                    Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
-                            + "finisher.accept(\"" + result.uri + "\"");
-                }
-                finisher.accept(result.uri);
-            } catch (Exception e) {
-                Log.d(TAG, "Failed to store screenshot", e);
-                if (DEBUG_CALLBACK) {
-                    Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
-                }
-                finisher.accept(null);
-            }
-        }, mMainExecutor);
-    }
-
-    /**
-     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
-     */
-    private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
-        if (uri == null) {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
-            mNotificationsController.notifyScreenshotError(
-                    R.string.screenshot_failed_to_save_text);
-        } else {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
-            if (mUserManager.isManagedProfile(owner.getIdentifier())) {
-                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0,
-                        mPackageName);
-            }
-        }
-    }
-
-    private boolean isUserSetupComplete(UserHandle owner) {
-        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
-                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
-    }
-
-    private Rect getFullScreenRect() {
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        mDisplay.getRealMetrics(displayMetrics);
-        return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
-    }
-
-    /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
-    private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets,
-            Rect screenBounds) {
-        int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right;
-        int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom;
-
-        if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
-                || bitmap.getHeight() == 0) {
-            if (DEBUG_UI) {
-                Log.e(TAG, "Provided bitmap and insets create degenerate region: "
-                        + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets);
-            }
-            return false;
-        }
-
-        float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight;
-        float boundsAspect = ((float) screenBounds.width()) / screenBounds.height();
-
-        boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f;
-        if (DEBUG_UI) {
-            Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect
-                    + ", bounds: " + boundsAspect);
-        }
-        return matchWithinTolerance;
-    }
-
-    /** Injectable factory to create screenshot controller instances for a specific display. */
-    @AssistedFactory
-    public interface Factory extends InteractiveScreenshotHandler.Factory {
-        /**
-         * Creates an instance of the controller for that specific display.
-         *
-         * @param display                 display to capture
-         */
-        ScreenshotController create(Display display);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
new file mode 100644
index 0000000..29208f8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
@@ -0,0 +1,632 @@
+/*
+ * 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.animation.Animator
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Insets
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Process
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.Display
+import android.view.ScrollCaptureResponse
+import android.view.ViewRootImpl.ActivityConfigCallback
+import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
+import android.widget.Toast
+import android.window.WindowContext
+import androidx.core.animation.doOnEnd
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.clipboardoverlay.ClipboardOverlayController
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ActionIntentCreator.createLongScreenshotIntent
+import com.android.systemui.screenshot.ScreenshotShelfViewProxy.ScreenshotViewCallback
+import com.android.systemui.screenshot.scroll.ScrollCaptureController.LongScreenshot
+import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor
+import com.android.systemui.util.Assert
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.function.Consumer
+import javax.inject.Provider
+import kotlin.math.abs
+
+/** Controls the state and flow for screenshots. */
+class ScreenshotController
+@AssistedInject
+internal constructor(
+    appContext: Context,
+    screenshotWindowFactory: ScreenshotWindow.Factory,
+    viewProxyFactory: ScreenshotShelfViewProxy.Factory,
+    screenshotNotificationsControllerFactory: ScreenshotNotificationsController.Factory,
+    screenshotActionsControllerFactory: ScreenshotActionsController.Factory,
+    actionExecutorFactory: ActionExecutor.Factory,
+    screenshotSoundControllerProvider: Provider<ScreenshotSoundController?>,
+    private val uiEventLogger: UiEventLogger,
+    private val imageExporter: ImageExporter,
+    private val imageCapture: ImageCapture,
+    private val scrollCaptureExecutor: ScrollCaptureExecutor,
+    private val screenshotHandler: TimeoutHandler,
+    private val broadcastSender: BroadcastSender,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val userManager: UserManager,
+    private val assistContentRequester: AssistContentRequester,
+    private val messageContainerController: MessageContainerController,
+    private val announcementResolver: AnnouncementResolver,
+    @Main private val mainExecutor: Executor,
+    @Assisted private val display: Display,
+) : InteractiveScreenshotHandler {
+    private val context: WindowContext
+    private val viewProxy: ScreenshotShelfViewProxy
+    private val notificationController =
+        screenshotNotificationsControllerFactory.create(display.displayId)
+    private val bgExecutor: ExecutorService = Executors.newSingleThreadExecutor()
+    private val actionsController: ScreenshotActionsController
+    private val window: ScreenshotWindow
+    private val actionExecutor: ActionExecutor
+    private val copyBroadcastReceiver: BroadcastReceiver
+
+    private var screenshotSoundController: ScreenshotSoundController? = null
+    private var screenBitmap: Bitmap? = null
+    private var screenshotTakenInPortrait = false
+    private var screenshotAnimation: Animator? = null
+    private var currentRequestCallback: TakeScreenshotService.RequestCallback? = null
+    private var packageName = ""
+
+    /** Tracks config changes that require re-creating UI */
+    private val configChanges =
+        InterestingConfigChanges(
+            ActivityInfo.CONFIG_ORIENTATION or
+                ActivityInfo.CONFIG_LAYOUT_DIRECTION or
+                ActivityInfo.CONFIG_LOCALE or
+                ActivityInfo.CONFIG_UI_MODE or
+                ActivityInfo.CONFIG_SCREEN_LAYOUT or
+                ActivityInfo.CONFIG_ASSETS_PATHS
+        )
+
+    init {
+        screenshotHandler.defaultTimeoutMillis = SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS
+
+        window = screenshotWindowFactory.create(display)
+        context = window.getContext()
+
+        viewProxy = viewProxyFactory.getProxy(context, display.displayId)
+
+        screenshotHandler.setOnTimeoutRunnable {
+            if (LogConfig.DEBUG_UI) {
+                Log.d(TAG, "Corner timeout hit")
+            }
+            viewProxy.requestDismissal(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT)
+        }
+
+        configChanges.applyNewConfig(appContext.resources)
+        reloadAssets()
+
+        actionExecutor = actionExecutorFactory.create(window.window, viewProxy) { finishDismiss() }
+        actionsController = screenshotActionsControllerFactory.getController(actionExecutor)
+
+        // Sound is only reproduced from the controller of the default display.
+        screenshotSoundController =
+            if (display.displayId == Display.DEFAULT_DISPLAY) {
+                screenshotSoundControllerProvider.get()
+            } else {
+                null
+            }
+
+        copyBroadcastReceiver =
+            object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    if (ClipboardOverlayController.COPY_OVERLAY_ACTION == intent.action) {
+                        viewProxy.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
+                    }
+                }
+            }
+        broadcastDispatcher.registerReceiver(
+            copyBroadcastReceiver,
+            IntentFilter(ClipboardOverlayController.COPY_OVERLAY_ACTION),
+            null,
+            null,
+            Context.RECEIVER_NOT_EXPORTED,
+            ClipboardOverlayController.SELF_PERMISSION,
+        )
+    }
+
+    override fun handleScreenshot(
+        screenshot: ScreenshotData,
+        finisher: Consumer<Uri?>,
+        requestCallback: TakeScreenshotService.RequestCallback,
+    ) {
+        Assert.isMainThread()
+
+        currentRequestCallback = requestCallback
+        if (screenshot.type == TAKE_SCREENSHOT_FULLSCREEN && screenshot.bitmap == null) {
+            val bounds = fullScreenRect
+            screenshot.bitmap = imageCapture.captureDisplay(display.displayId, bounds)
+            screenshot.screenBounds = bounds
+        }
+
+        val currentBitmap = screenshot.bitmap
+        if (currentBitmap == null) {
+            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null")
+            notificationController.notifyScreenshotError(R.string.screenshot_failed_to_capture_text)
+            currentRequestCallback?.reportError()
+            return
+        }
+
+        screenBitmap = currentBitmap
+        val oldPackageName = packageName
+        packageName = screenshot.packageNameString
+
+        if (!isUserSetupComplete(Process.myUserHandle())) {
+            Log.w(TAG, "User setup not complete, displaying toast only")
+            // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
+            // and sharing shouldn't be exposed to the user.
+            saveScreenshotAndToast(screenshot, finisher)
+            return
+        }
+
+        broadcastSender.sendBroadcast(
+            Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
+            ClipboardOverlayController.SELF_PERMISSION,
+        )
+
+        screenshotTakenInPortrait =
+            context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
+
+        // Optimizations
+        currentBitmap.setHasAlpha(false)
+        currentBitmap.prepareToDraw()
+
+        prepareViewForNewScreenshot(screenshot, oldPackageName)
+        val requestId = actionsController.setCurrentScreenshot(screenshot)
+        saveScreenshotInBackground(screenshot, requestId, finisher) { result ->
+            if (result.uri != null) {
+                val savedScreenshot =
+                    ScreenshotSavedResult(
+                        result.uri,
+                        screenshot.getUserOrDefault(),
+                        result.timestamp,
+                    )
+                actionsController.setCompletedScreenshot(requestId, savedScreenshot)
+            }
+        }
+
+        if (screenshot.taskId >= 0) {
+            assistContentRequester.requestAssistContent(screenshot.taskId) { assistContent ->
+                actionsController.onAssistContent(requestId, assistContent)
+            }
+        } else {
+            actionsController.onAssistContent(requestId, null)
+        }
+
+        // The window is focusable by default
+        window.setFocusable(true)
+        viewProxy.requestFocus()
+
+        enqueueScrollCaptureRequest(requestId, screenshot.userHandle!!)
+
+        window.attachWindow()
+
+        val showFlash: Boolean
+        if (screenshot.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+            if (aspectRatiosMatch(currentBitmap, screenshot.insets, screenshot.screenBounds)) {
+                showFlash = false
+            } else {
+                showFlash = true
+                screenshot.insets = Insets.NONE
+                screenshot.screenBounds = Rect(0, 0, currentBitmap.width, currentBitmap.height)
+            }
+        } else {
+            showFlash = true
+        }
+
+        // screenshot.screenBounds is expected to be non-null in all cases at this point
+        val bounds =
+            screenshot.screenBounds ?: Rect(0, 0, currentBitmap.width, currentBitmap.height)
+
+        viewProxy.prepareEntranceAnimation {
+            startAnimation(bounds, showFlash) {
+                messageContainerController.onScreenshotTaken(screenshot)
+            }
+        }
+
+        viewProxy.screenshot = screenshot
+    }
+
+    private fun prepareViewForNewScreenshot(screenshot: ScreenshotData, oldPackageName: String?) {
+        window.whenWindowAttached {
+            announcementResolver.getScreenshotAnnouncement(screenshot.userHandle!!.identifier) {
+                viewProxy.announceForAccessibility(it)
+            }
+        }
+
+        viewProxy.reset()
+
+        if (viewProxy.isAttachedToWindow) {
+            // if we didn't already dismiss for another reason
+            if (!viewProxy.isDismissing) {
+                uiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, oldPackageName)
+            }
+            if (LogConfig.DEBUG_WINDOW) {
+                Log.d(
+                    TAG,
+                    "saveScreenshot: screenshotView is already attached, resetting. " +
+                        "(dismissing=${viewProxy.isDismissing})",
+                )
+            }
+        }
+
+        viewProxy.packageName = packageName
+    }
+
+    /**
+     * Requests the view to dismiss the current screenshot (may be ignored, if screenshot is already
+     * being dismissed)
+     */
+    override fun requestDismissal(event: ScreenshotEvent) {
+        viewProxy.requestDismissal(event)
+    }
+
+    override fun isPendingSharedTransition(): Boolean {
+        return actionExecutor.isPendingSharedTransition
+    }
+
+    // Any cleanup needed when the service is being destroyed.
+    override fun onDestroy() {
+        removeWindow()
+        releaseMediaPlayer()
+        releaseContext()
+        bgExecutor.shutdown()
+    }
+
+    /** Release the constructed window context. */
+    private fun releaseContext() {
+        broadcastDispatcher.unregisterReceiver(copyBroadcastReceiver)
+        context.release()
+    }
+
+    private fun releaseMediaPlayer() {
+        screenshotSoundController?.releaseScreenshotSoundAsync()
+    }
+
+    /** Update resources on configuration change. Reinflate for theme/color changes. */
+    private fun reloadAssets() {
+        if (LogConfig.DEBUG_UI) {
+            Log.d(TAG, "reloadAssets()")
+        }
+
+        messageContainerController.setView(viewProxy.view)
+        viewProxy.callbacks =
+            object : ScreenshotViewCallback {
+                override fun onUserInteraction() {
+                    if (LogConfig.DEBUG_INPUT) {
+                        Log.d(TAG, "onUserInteraction")
+                    }
+                    screenshotHandler.resetTimeout()
+                }
+
+                override fun onDismiss() {
+                    finishDismiss()
+                }
+
+                override fun onTouchOutside() {
+                    // TODO(159460485): Remove this when focus is handled properly in the system
+                    window.setFocusable(false)
+                }
+            }
+
+        if (LogConfig.DEBUG_WINDOW) {
+            Log.d(TAG, "setContentView: " + viewProxy.view)
+        }
+        window.setContentView(viewProxy.view)
+    }
+
+    private fun enqueueScrollCaptureRequest(requestId: UUID, owner: UserHandle) {
+        // Wait until this window is attached to request because it is
+        // the reference used to locate the target window (below).
+        window.whenWindowAttached {
+            requestScrollCapture(requestId, owner)
+            window.setActivityConfigCallback(
+                object : ActivityConfigCallback {
+                    override fun onConfigurationChanged(
+                        overrideConfig: Configuration,
+                        newDisplayId: Int,
+                    ) {
+                        if (configChanges.applyNewConfig(context.resources)) {
+                            // Hide the scroll chip until we know it's available in this
+                            // orientation
+                            actionsController.onScrollChipInvalidated()
+                            // Delay scroll capture eval a bit to allow the underlying activity
+                            // to set up in the new orientation.
+                            screenshotHandler.postDelayed(
+                                { requestScrollCapture(requestId, owner) },
+                                150,
+                            )
+                            viewProxy.updateInsets(window.getWindowInsets())
+                            // Screenshot animation calculations won't be valid anymore, so just end
+                            screenshotAnimation?.let { currentAnimation ->
+                                if (currentAnimation.isRunning) {
+                                    currentAnimation.end()
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+    }
+
+    private fun requestScrollCapture(requestId: UUID, owner: UserHandle) {
+        scrollCaptureExecutor.requestScrollCapture(display.displayId, window.getWindowToken()) {
+            response: ScrollCaptureResponse ->
+            uiEventLogger.log(
+                ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
+                0,
+                response.packageName,
+            )
+            actionsController.onScrollChipReady(requestId) {
+                onScrollButtonClicked(owner, response)
+            }
+        }
+    }
+
+    private fun onScrollButtonClicked(owner: UserHandle, response: ScrollCaptureResponse) {
+        if (LogConfig.DEBUG_INPUT) {
+            Log.d(TAG, "scroll chip tapped")
+        }
+        uiEventLogger.log(
+            ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED,
+            0,
+            response.packageName,
+        )
+        val newScreenshot = imageCapture.captureDisplay(display.displayId, null)
+        if (newScreenshot == null) {
+            Log.e(TAG, "Failed to capture current screenshot for scroll transition!")
+            return
+        }
+        // delay starting scroll capture to make sure scrim is up before the app moves
+        viewProxy.prepareScrollingTransition(response, newScreenshot, screenshotTakenInPortrait) {
+            executeBatchScrollCapture(response, owner)
+        }
+    }
+
+    private fun executeBatchScrollCapture(response: ScrollCaptureResponse, owner: UserHandle) {
+        scrollCaptureExecutor.executeBatchScrollCapture(
+            response,
+            {
+                val intent = createLongScreenshotIntent(owner, context)
+                context.startActivity(intent)
+            },
+            { viewProxy.restoreNonScrollingUi() },
+            { transitionDestination: Rect, onTransitionEnd: Runnable, longScreenshot: LongScreenshot
+                ->
+                viewProxy.startLongScreenshotTransition(
+                    transitionDestination,
+                    onTransitionEnd,
+                    longScreenshot,
+                )
+            },
+        )
+    }
+
+    override fun removeWindow() {
+        window.removeWindow()
+        viewProxy.stopInputListening()
+    }
+
+    private fun playCameraSoundIfNeeded() {
+        // the controller is not-null only on the default display controller
+        screenshotSoundController?.playScreenshotSoundAsync()
+    }
+
+    /**
+     * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
+     * failure).
+     */
+    private fun saveScreenshotAndToast(screenshot: ScreenshotData, finisher: Consumer<Uri?>) {
+        // Play the shutter sound to notify that we've taken a screenshot
+        playCameraSoundIfNeeded()
+
+        saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher) {
+            result: ImageExporter.Result ->
+            if (result.uri != null) {
+                screenshotHandler.post {
+                    Toast.makeText(context, R.string.screenshot_saved_title, Toast.LENGTH_SHORT)
+                        .show()
+                }
+            }
+        }
+    }
+
+    /** Starts the animation after taking the screenshot */
+    private fun startAnimation(
+        screenRect: Rect,
+        showFlash: Boolean,
+        onAnimationComplete: Runnable?,
+    ) {
+        screenshotAnimation?.let { currentAnimation ->
+            if (currentAnimation.isRunning) {
+                currentAnimation.cancel()
+            }
+        }
+
+        screenshotAnimation =
+            viewProxy.createScreenshotDropInAnimation(screenRect, showFlash).apply {
+                doOnEnd { onAnimationComplete?.run() }
+                // Play the shutter sound to notify that we've taken a screenshot
+                playCameraSoundIfNeeded()
+                if (LogConfig.DEBUG_ANIM) {
+                    Log.d(TAG, "starting post-screenshot animation")
+                }
+                start()
+            }
+    }
+
+    /** Reset screenshot view and then call onCompleteRunnable */
+    private fun finishDismiss() {
+        Log.d(TAG, "finishDismiss")
+        actionsController.endScreenshotSession()
+        scrollCaptureExecutor.close()
+        currentRequestCallback?.onFinish()
+        currentRequestCallback = null
+        viewProxy.reset()
+        removeWindow()
+        screenshotHandler.cancelTimeout()
+    }
+
+    private fun saveScreenshotInBackground(
+        screenshot: ScreenshotData,
+        requestId: UUID,
+        finisher: Consumer<Uri?>,
+        onResult: Consumer<ImageExporter.Result>,
+    ) {
+        val future =
+            imageExporter.export(
+                bgExecutor,
+                requestId,
+                screenshot.bitmap,
+                screenshot.getUserOrDefault(),
+                display.displayId,
+            )
+        future.addListener(
+            {
+                try {
+                    val result = future.get()
+                    Log.d(TAG, "Saved screenshot: $result")
+                    logScreenshotResultStatus(result.uri, screenshot.userHandle!!)
+                    onResult.accept(result)
+                    if (LogConfig.DEBUG_CALLBACK) {
+                        Log.d(TAG, "finished bg processing, calling back with uri: ${result.uri}")
+                    }
+                    finisher.accept(result.uri)
+                } catch (e: Exception) {
+                    Log.d(TAG, "Failed to store screenshot", e)
+                    if (LogConfig.DEBUG_CALLBACK) {
+                        Log.d(TAG, "calling back with uri: null")
+                    }
+                    finisher.accept(null)
+                }
+            },
+            mainExecutor,
+        )
+    }
+
+    /** Logs success/failure of the screenshot saving task, and shows an error if it failed. */
+    private fun logScreenshotResultStatus(uri: Uri?, owner: UserHandle) {
+        if (uri == null) {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, packageName)
+            notificationController.notifyScreenshotError(R.string.screenshot_failed_to_save_text)
+        } else {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, packageName)
+            if (userManager.isManagedProfile(owner.identifier)) {
+                uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, packageName)
+            }
+        }
+    }
+
+    private fun isUserSetupComplete(owner: UserHandle): Boolean {
+        return Settings.Secure.getInt(
+            context.createContextAsUser(owner, 0).contentResolver,
+            SETTINGS_SECURE_USER_SETUP_COMPLETE,
+            0,
+        ) == 1
+    }
+
+    private val fullScreenRect: Rect
+        get() {
+            val displayMetrics = DisplayMetrics()
+            display.getRealMetrics(displayMetrics)
+            return Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
+        }
+
+    /** Injectable factory to create screenshot controller instances for a specific display. */
+    @AssistedFactory
+    interface Factory : InteractiveScreenshotHandler.Factory {
+        /**
+         * Creates an instance of the controller for that specific display.
+         *
+         * @param display display to capture
+         */
+        override fun create(display: Display): ScreenshotController
+    }
+
+    companion object {
+        private val TAG: String = LogConfig.logTag(ScreenshotController::class.java)
+
+        // From WizardManagerHelper.java
+        private const val SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"
+
+        const val SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS: Int = 6000
+
+        /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
+        private fun aspectRatiosMatch(
+            bitmap: Bitmap,
+            bitmapInsets: Insets,
+            screenBounds: Rect?,
+        ): Boolean {
+            if (screenBounds == null) {
+                return false
+            }
+            val insettedWidth = bitmap.width - bitmapInsets.left - bitmapInsets.right
+            val insettedHeight = bitmap.height - bitmapInsets.top - bitmapInsets.bottom
+
+            if (
+                insettedHeight == 0 || insettedWidth == 0 || bitmap.width == 0 || bitmap.height == 0
+            ) {
+                if (LogConfig.DEBUG_UI) {
+                    Log.e(
+                        TAG,
+                        "Provided bitmap and insets create degenerate region: " +
+                            "${bitmap.width} x ${bitmap.height} $bitmapInsets",
+                    )
+                }
+                return false
+            }
+
+            val insettedBitmapAspect = insettedWidth.toFloat() / insettedHeight
+            val boundsAspect = screenBounds.width().toFloat() / screenBounds.height()
+
+            val matchWithinTolerance = abs((insettedBitmapAspect - boundsAspect).toDouble()) < 0.1f
+            if (LogConfig.DEBUG_UI) {
+                Log.d(
+                    TAG,
+                    "aspectRatiosMatch: don't match bitmap: " +
+                        "$insettedBitmapAspect, bounds: $boundsAspect",
+                )
+            }
+            return matchWithinTolerance
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 50215af..c1ea3ad 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -67,7 +67,7 @@
     shelfViewBinder: ScreenshotShelfViewBinder,
     private val thumbnailObserver: ThumbnailObserver,
     @Assisted private val context: Context,
-    @Assisted private val displayId: Int
+    @Assisted private val displayId: Int,
 ) {
 
     interface ScreenshotViewCallback {
@@ -117,7 +117,7 @@
             animationController,
             LayoutInflater.from(context),
             onDismissalRequested = { event, velocity -> requestDismissal(event, velocity) },
-            onUserInteraction = { callbacks?.onUserInteraction() }
+            onUserInteraction = { callbacks?.onUserInteraction() },
         )
         view.updateInsets(windowManager.currentWindowMetrics.windowInsets)
         addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
@@ -130,7 +130,7 @@
         screenshotPreview = view.screenshotPreview
         thumbnailObserver.setViews(
             view.blurredScreenshotPreview,
-            view.requireViewById(R.id.screenshot_preview_border)
+            view.requireViewById(R.id.screenshot_preview_border),
         )
         view.addOnAttachStateChangeListener(
             object : View.OnAttachStateChangeListener {
@@ -204,7 +204,6 @@
 
     fun prepareScrollingTransition(
         response: ScrollCaptureResponse,
-        screenBitmap: Bitmap, // unused
         newScreenshot: Bitmap,
         screenshotTakenInPortrait: Boolean,
         onTransitionPrepared: Runnable,
@@ -224,7 +223,7 @@
                 0,
                 0,
                 context.resources.displayMetrics.widthPixels,
-                context.resources.displayMetrics.heightPixels
+                context.resources.displayMetrics.heightPixels,
             )
         )
         return r
@@ -239,7 +238,7 @@
             animationController.runLongScreenshotTransition(
                 transitionDestination,
                 longScreenshot,
-                onTransitionEnd
+                onTransitionEnd,
             )
         transitionAnimation.doOnEnd { callbacks?.onDismiss() }
         transitionAnimation.start()
@@ -295,7 +294,7 @@
                         .findOnBackInvokedDispatcher()
                         ?.registerOnBackInvokedCallback(
                             OnBackInvokedDispatcher.PRIORITY_DEFAULT,
-                            onBackInvokedCallback
+                            onBackInvokedCallback,
                         )
                 }