Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6df1d0ab authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Extract screenshot window code

Bug: 354711957
Flag: com.android.systemui.screenshot_ui_controller_refactor
Test: manual
Change-Id: Ida73a0fb36ba4b405ed6555a42ed2bd0b63155a4
parent 22a5dfe1
Loading
Loading
Loading
Loading
+15 −117
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.screenshot;

import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;

import static com.android.systemui.Flags.screenshotSaveImageExporter;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
@@ -31,7 +30,6 @@ import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERAC

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
@@ -52,17 +50,12 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.ScrollCaptureResponse;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.WindowContext;

import com.android.internal.logging.UiEventLogger;
import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastSender;
@@ -115,11 +108,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
    private final BroadcastDispatcher mBroadcastDispatcher;
    private final ScreenshotActionsController mActionsController;

    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mWindowLayoutParams;
    @Nullable
    private final ScreenshotSoundController mScreenshotSoundController;
    private final PhoneWindow mWindow;
    private final ScreenshotWindow mWindow;
    private final Display mDisplay;
    private final ScrollCaptureExecutor mScrollCaptureExecutor;
    private final ScreenshotNotificationSmartActionsProvider
@@ -135,8 +126,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
    private Bitmap mScreenBitmap;
    private SaveImageInBackgroundTask mSaveInBgTask;
    private boolean mScreenshotTakenInPortrait;
    private boolean mAttachRequested;
    private boolean mDetachRequested;
    private Animator mScreenshotAnimation;
    private RequestCallback mCurrentRequestCallback;
    private String mPackageName = "";
@@ -155,7 +144,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
    @AssistedInject
    ScreenshotController(
            Context context,
            WindowManager windowManager,
            ScreenshotWindow.Factory screenshotWindowFactory,
            FeatureFlags flags,
            ScreenshotShelfViewProxy.Factory viewProxyFactory,
            ScreenshotSmartActions screenshotSmartActions,
@@ -195,9 +184,8 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
        mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);

        mDisplay = display;
        mWindowManager = windowManager;
        final Context displayContext = context.createDisplayContext(display);
        mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
        mWindow = screenshotWindowFactory.create(mDisplay);
        mContext = mWindow.getContext();
        mFlags = flags;
        mUserManager = userManager;
        mMessageContainerController = messageContainerController;
@@ -213,17 +201,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
            mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT);
        });

        // Setup the window that we are going to use
        mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
        mWindowLayoutParams.setTitle("ScreenshotAnimation");

        mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
        mWindow.setWindowManager(mWindowManager, null, null);

        mConfigChanges.applyNewConfig(context.getResources());
        reloadAssets();

        mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy,
        mActionExecutor = actionExecutorFactory.create(mWindow.getWindow(), mViewProxy,
                () -> {
                    finishDismiss();
                    return Unit.INSTANCE;
@@ -318,12 +299,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
        }

        // The window is focusable by default
        setWindowFocusable(true);
        mWindow.setFocusable(true);
        mViewProxy.requestFocus();

        enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle());

        attachWindow();
        mWindow.attachWindow();

        boolean showFlash;
        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
@@ -347,13 +328,10 @@ public class ScreenshotController implements InteractiveScreenshotHandler {

        mViewProxy.setScreenshot(screenshot);

        // ignore system bar insets for the purpose of window layout
        mWindow.getDecorView().setOnApplyWindowInsetsListener(
                (v, insets) -> WindowInsets.CONSUMED);
    }

    void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
        withWindowAttached(() -> {
        mWindow.whenWindowAttached(() -> {
            mAnnouncementResolver.getScreenshotAnnouncement(
                    screenshot.getUserHandle().getIdentifier(),
                    announcement -> {
@@ -444,7 +422,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
            @Override
            public void onTouchOutside() {
                // TODO(159460485): Remove this when focus is handled properly in the system
                setWindowFocusable(false);
                mWindow.setFocusable(false);
            }
        });

@@ -457,9 +435,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
    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).
        withWindowAttached(() -> {
        mWindow.whenWindowAttached(() -> {
            requestScrollCapture(requestId, owner);
            mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
            mWindow.setActivityConfigCallback(
                    new ViewRootImpl.ActivityConfigCallback() {
                        @Override
                        public void onConfigurationChanged(Configuration overrideConfig,
@@ -472,8 +450,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
                                // to set up in the new orientation.
                                mScreenshotHandler.postDelayed(
                                        () -> requestScrollCapture(requestId, owner), 150);
                                mViewProxy.updateInsets(
                                        mWindowManager.getCurrentWindowMetrics().getWindowInsets());
                                mViewProxy.updateInsets(mWindow.getWindowInsets());
                                // Screenshot animation calculations won't be valid anymore,
                                // so just end
                                if (mScreenshotAnimation != null
@@ -489,7 +466,7 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
    private void requestScrollCapture(UUID requestId, UserHandle owner) {
        mScrollCaptureExecutor.requestScrollCapture(
                mDisplay.getDisplayId(),
                mWindow.getDecorView().getWindowToken(),
                mWindow.getWindowToken(),
                (response) -> {
                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
                            0, response.getPackageName());
@@ -528,61 +505,9 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
                mViewProxy::startLongScreenshotTransition);
    }

    private void withWindowAttached(Runnable action) {
        View decorView = mWindow.getDecorView();
        if (decorView.isAttachedToWindow()) {
            action.run();
        } else {
            decorView.getViewTreeObserver().addOnWindowAttachListener(
                    new ViewTreeObserver.OnWindowAttachListener() {
                        @Override
                        public void onWindowAttached() {
                            mAttachRequested = false;
                            decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
                            action.run();
                        }

                        @Override
                        public void onWindowDetached() {
                        }
                    });

        }
    }

    @MainThread
    private void attachWindow() {
        View decorView = mWindow.getDecorView();
        if (decorView.isAttachedToWindow() || mAttachRequested) {
            return;
        }
        if (DEBUG_WINDOW) {
            Log.d(TAG, "attachWindow");
        }
        mAttachRequested = true;
        mWindowManager.addView(decorView, mWindowLayoutParams);
        decorView.requestApplyInsets();

        ViewGroup layout = decorView.requireViewById(android.R.id.content);
        layout.setClipChildren(false);
        layout.setClipToPadding(false);
    }

    @Override
    public void removeWindow() {
        final View decorView = mWindow.peekDecorView();
        if (decorView != null && decorView.isAttachedToWindow()) {
            if (DEBUG_WINDOW) {
                Log.d(TAG, "Removing screenshot window");
            }
            mWindowManager.removeViewImmediate(decorView);
            mDetachRequested = false;
        }
        if (mAttachRequested && !mDetachRequested) {
            mDetachRequested = true;
            withWindowAttached(this::removeWindow);
        }

        mWindow.removeWindow();
        mViewProxy.stopInputListening();
    }

@@ -759,33 +684,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
    }

    /**
     * Updates the window focusability.  If the window is already showing, then it updates the
     * window immediately, otherwise the layout params will be applied when the window is next
     * shown.
     */
    private void setWindowFocusable(boolean focusable) {
        if (DEBUG_WINDOW) {
            Log.d(TAG, "setWindowFocusable: " + focusable);
        }
        int flags = mWindowLayoutParams.flags;
        if (focusable) {
            mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        } else {
            mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        }
        if (mWindowLayoutParams.flags == flags) {
            if (DEBUG_WINDOW) {
                Log.d(TAG, "setWindowFocusable: skipping, already " + focusable);
            }
            return;
        }
        final View decorView = mWindow.peekDecorView();
        if (decorView != null && decorView.isAttachedToWindow()) {
            mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
        }
    }

    private Rect getFullScreenRect() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        mDisplay.getRealMetrics(displayMetrics);
@@ -826,6 +724,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler {
         *
         * @param display                 display to capture
         */
        LegacyScreenshotController create(Display display);
        ScreenshotController create(Display display);
    }
}
+194 −0
Original line number Diff line number Diff line
/*
 * 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.R
import android.annotation.MainThread
import android.content.Context
import android.graphics.PixelFormat
import android.os.IBinder
import android.util.Log
import android.view.Display
import android.view.View
import android.view.ViewGroup
import android.view.ViewRootImpl
import android.view.ViewTreeObserver.OnWindowAttachListener
import android.view.Window
import android.view.WindowInsets
import android.view.WindowManager
import android.window.WindowContext
import com.android.internal.policy.PhoneWindow
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/** Creates and manages the window in which the screenshot UI is displayed. */
class ScreenshotWindow
@AssistedInject
constructor(
    private val windowManager: WindowManager,
    private val context: Context,
    @Assisted private val display: Display,
) {

    val window: PhoneWindow =
        PhoneWindow(
            context
                .createDisplayContext(display)
                .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null)
        )
    private val params =
        WindowManager.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                0, /* xpos */
                0, /* ypos */
                WindowManager.LayoutParams.TYPE_SCREENSHOT,
                WindowManager.LayoutParams.FLAG_FULLSCREEN or
                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
                    WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                PixelFormat.TRANSLUCENT
            )
            .apply {
                layoutInDisplayCutoutMode =
                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
                setFitInsetsTypes(0)
                // This is needed to let touches pass through outside the touchable areas
                privateFlags =
                    privateFlags or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
                title = "ScreenshotUI"
            }
    private var attachRequested: Boolean = false
    private var detachRequested: Boolean = false

    init {
        window.requestFeature(Window.FEATURE_NO_TITLE)
        window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
        window.setBackgroundDrawableResource(R.color.transparent)
        window.setWindowManager(windowManager, null, null)
    }

    @MainThread
    fun attachWindow() {
        val decorView: View = window.getDecorView()
        if (decorView.isAttachedToWindow || attachRequested) {
            return
        }
        if (LogConfig.DEBUG_WINDOW) {
            Log.d(TAG, "attachWindow")
        }
        attachRequested = true
        windowManager.addView(decorView, params)

        decorView.requestApplyInsets()
        decorView.requireViewById<ViewGroup>(R.id.content).apply {
            clipChildren = false
            clipToPadding = false
            // ignore system bar insets for the purpose of window layout
            setOnApplyWindowInsetsListener { _, _ -> WindowInsets.CONSUMED }
        }
    }

    fun whenWindowAttached(action: Runnable) {
        val decorView: View = window.getDecorView()
        if (decorView.isAttachedToWindow) {
            action.run()
        } else {
            decorView
                .getViewTreeObserver()
                .addOnWindowAttachListener(
                    object : OnWindowAttachListener {
                        override fun onWindowAttached() {
                            attachRequested = false
                            decorView.getViewTreeObserver().removeOnWindowAttachListener(this)
                            action.run()
                        }

                        override fun onWindowDetached() {}
                    }
                )
        }
    }

    fun removeWindow() {
        val decorView: View? = window.peekDecorView()
        if (decorView != null && decorView.isAttachedToWindow) {
            if (LogConfig.DEBUG_WINDOW) {
                Log.d(TAG, "Removing screenshot window")
            }
            windowManager.removeViewImmediate(decorView)
            detachRequested = false
        }
        if (attachRequested && !detachRequested) {
            detachRequested = true
            whenWindowAttached { removeWindow() }
        }
    }

    /**
     * Updates the window focusability. If the window is already showing, then it updates the window
     * immediately, otherwise the layout params will be applied when the window is next shown.
     */
    fun setFocusable(focusable: Boolean) {
        if (LogConfig.DEBUG_WINDOW) {
            Log.d(TAG, "setWindowFocusable: $focusable")
        }
        val flags: Int = params.flags
        if (focusable) {
            params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv()
        } else {
            params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        }
        if (params.flags == flags) {
            if (LogConfig.DEBUG_WINDOW) {
                Log.d(TAG, "setWindowFocusable: skipping, already $focusable")
            }
            return
        }
        window.peekDecorView()?.also {
            if (it.isAttachedToWindow) {
                windowManager.updateViewLayout(it, params)
            }
        }
    }

    fun getContext(): WindowContext = window.context as WindowContext

    fun getWindowToken(): IBinder = window.decorView.windowToken

    fun getWindowInsets(): WindowInsets = windowManager.currentWindowMetrics.windowInsets

    fun setContentView(view: View) {
        window.setContentView(view)
    }

    fun setActivityConfigCallback(callback: ViewRootImpl.ActivityConfigCallback) {
        window.peekDecorView().viewRootImpl.setActivityConfigCallback(callback)
    }

    @AssistedFactory
    interface Factory {
        fun create(display: Display): ScreenshotWindow
    }

    companion object {
        private const val TAG = "ScreenshotWindow"
    }
}