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

Commit 6e228244 authored by Nicolò Mazzucato's avatar Nicolò Mazzucato Committed by Android (Google) Code Review
Browse files

Merge "Capture screenshots from all displays (flag-guarded)" into main

parents 0b8d3d01 33115b5f
Loading
Loading
Loading
Loading
+14 −10
Original line number Diff line number Diff line
@@ -28,8 +28,18 @@ import kotlinx.coroutines.launch
import java.util.function.Consumer
import javax.inject.Inject

/** Processes a screenshot request sent from [ScreenshotHelper]. */
interface ScreenshotRequestProcessor {
    /**
 * Processes a screenshot request sent from {@link ScreenshotHelper}.
     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
     *
     * @param screenshot the screenshot to process
     */
    suspend fun process(screenshot: ScreenshotData): ScreenshotData
}

/**
 * Implementation of [ScreenshotRequestProcessor]
 */
@SysUISingleton
class RequestProcessor @Inject constructor(
@@ -38,7 +48,7 @@ class RequestProcessor @Inject constructor(
        private val flags: FeatureFlags,
        /** For the Java Async version, to invoke the callback. */
        @Application private val mainScope: CoroutineScope
) {
) : ScreenshotRequestProcessor {
    /**
     * Inspects the incoming request, returning a potentially modified request depending on policy.
     *
@@ -57,7 +67,6 @@ class RequestProcessor @Inject constructor(
        // regardless of the managed profile status.

        if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {

            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
            Log.d(TAG, "findPrimaryContent: $info")

@@ -99,12 +108,7 @@ class RequestProcessor @Inject constructor(
        }
    }

    /**
     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
     *
     * @param screenshot the screenshot to process
     */
    suspend fun process(screenshot: ScreenshotData): ScreenshotData {
    override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
        var result = screenshot

        // Apply work profile screenshots policy:
@@ -116,7 +120,7 @@ class RequestProcessor @Inject constructor(
        // regardless of the managed profile status.

        if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE) {
            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
            val info = policy.findPrimaryContent(screenshot.displayId)
            Log.d(TAG, "findPrimaryContent: $info")
            result.taskId = info.taskId
            result.topComponent = info.component
+27 −18
Original line number Diff line number Diff line
@@ -100,11 +100,14 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.settings.DisplayTracker;
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 java.io.File;
import java.util.List;
import java.util.concurrent.CancellationException;
@@ -118,7 +121,6 @@ import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;

import javax.inject.Inject;

/**
 * Controls the state and flow for screenshots.
@@ -275,7 +277,7 @@ public class ScreenshotController {
    private final ScrollCaptureClient mScrollCaptureClient;
    private final PhoneWindow mWindow;
    private final DisplayManager mDisplayManager;
    private final DisplayTracker mDisplayTracker;
    private final int mDisplayId;
    private final ScrollCaptureController mScrollCaptureController;
    private final LongScreenshotData mLongScreenshotHolder;
    private final boolean mIsLowRamDevice;
@@ -314,7 +316,8 @@ public class ScreenshotController {
                    | ActivityInfo.CONFIG_SCREEN_LAYOUT
                    | ActivityInfo.CONFIG_ASSETS_PATHS);

    @Inject

    @AssistedInject
    ScreenshotController(
            Context context,
            FeatureFlags flags,
@@ -335,7 +338,7 @@ public class ScreenshotController {
            UserManager userManager,
            AssistContentRequester assistContentRequester,
            MessageContainerController messageContainerController,
            DisplayTracker displayTracker
            @Assisted int displayId
    ) {
        mScreenshotSmartActions = screenshotSmartActions;
        mNotificationsController = screenshotNotificationsController;
@@ -360,9 +363,9 @@ public class ScreenshotController {
            dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT);
        });

        mDisplayId = displayId;
        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
        mDisplayTracker = displayTracker;
        final Context displayContext = context.createDisplayContext(getDefaultDisplay());
        final Context displayContext = context.createDisplayContext(getDisplay());
        mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
        mWindowManager = mContext.getSystemService(WindowManager.class);
        mFlags = flags;
@@ -406,7 +409,7 @@ public class ScreenshotController {
        if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
            Rect bounds = getFullScreenRect();
            screenshot.setBitmap(
                    mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(), bounds));
                    mImageCapture.captureDisplay(mDisplayId, bounds));
            screenshot.setScreenBounds(bounds);
        }

@@ -638,7 +641,7 @@ public class ScreenshotController {
                setWindowFocusable(false);
            }
        }, mActionExecutor, mFlags);
        mScreenshotView.setDefaultDisplay(mDisplayTracker.getDefaultDisplayId());
        mScreenshotView.setDefaultDisplay(mDisplayId);
        mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());

        mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
@@ -727,8 +730,8 @@ public class ScreenshotController {
        if (mLastScrollCaptureRequest != null) {
            mLastScrollCaptureRequest.cancel(true);
        }
        final ListenableFuture<ScrollCaptureResponse> future =
                mScrollCaptureClient.request(mDisplayTracker.getDefaultDisplayId());
        final ListenableFuture<ScrollCaptureResponse> future = mScrollCaptureClient.request(
                mDisplayId);
        mLastScrollCaptureRequest = future;
        mLastScrollCaptureRequest.addListener(() ->
                onScrollCaptureResponseReady(future, owner), mMainExecutor);
@@ -758,9 +761,8 @@ public class ScreenshotController {
            final ScrollCaptureResponse response = mLastScrollCaptureResponse;
            mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
                DisplayMetrics displayMetrics = new DisplayMetrics();
                getDefaultDisplay().getRealMetrics(displayMetrics);
                Bitmap newScreenshot = mImageCapture.captureDisplay(
                        mDisplayTracker.getDefaultDisplayId(),
                getDisplay().getRealMetrics(displayMetrics);
                Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
                        new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));

                mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
@@ -825,7 +827,7 @@ public class ScreenshotController {
            try {
                WindowManagerGlobal.getWindowManagerService()
                        .overridePendingAppTransitionRemote(runner,
                                mDisplayTracker.getDefaultDisplayId());
                                mDisplayId);
            } catch (Exception e) {
                Log.e(TAG, "Error overriding screenshot app transition", e);
            }
@@ -1160,8 +1162,8 @@ public class ScreenshotController {
        }
    }

    private Display getDefaultDisplay() {
        return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId());
    private Display getDisplay() {
        return mDisplayManager.getDisplay(mDisplayId);
    }

    private boolean allowLongScreenshots() {
@@ -1170,7 +1172,7 @@ public class ScreenshotController {

    private Rect getFullScreenRect() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        getDefaultDisplay().getRealMetrics(displayMetrics);
        getDisplay().getRealMetrics(displayMetrics);
        return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
    }

@@ -1229,4 +1231,11 @@ public class ScreenshotController {
            };
        }
    }

    /** Injectable factory to create screenshot controller instances for a specific display. */
    @AssistedFactory
    public interface Factory {
        /** Creates an instance of the controller for that specific displayId. */
        ScreenshotController create(int displayId);
    }
}
+26 −15
Original line number Diff line number Diff line
@@ -6,12 +6,13 @@ import android.graphics.Insets
import android.graphics.Rect
import android.net.Uri
import android.os.UserHandle
import android.view.Display
import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.ScreenshotType
import androidx.annotation.VisibleForTesting
import com.android.internal.util.ScreenshotRequest

/** ScreenshotData represents the current state of a single screenshot being acquired. */
/** [ScreenshotData] represents the current state of a single screenshot being acquired. */
data class ScreenshotData(
    @ScreenshotType var type: Int,
    @ScreenshotSource var source: Int,
@@ -23,6 +24,7 @@ data class ScreenshotData(
    var taskId: Int,
    var insets: Insets,
    var bitmap: Bitmap?,
    var displayId: Int,
    /** App-provided URL representing the content the user was looking at in the screenshot. */
    var contextUrl: Uri? = null,
) {
@@ -31,22 +33,31 @@ data class ScreenshotData(

    companion object {
        @JvmStatic
        fun fromRequest(request: ScreenshotRequest): ScreenshotData {
            return ScreenshotData(
                request.type,
                request.source,
                if (request.userId >= 0) UserHandle.of(request.userId) else null,
                request.topComponent,
                request.boundsInScreen,
                request.taskId,
                request.insets,
                request.bitmap,
        fun fromRequest(request: ScreenshotRequest, displayId: Int = Display.DEFAULT_DISPLAY) =
            ScreenshotData(
                type = request.type,
                source = request.source,
                userHandle = if (request.userId >= 0) UserHandle.of(request.userId) else null,
                topComponent = request.topComponent,
                screenBounds = request.boundsInScreen,
                taskId = request.taskId,
                insets = request.insets,
                bitmap = request.bitmap,
                displayId = displayId,
            )
        }

        @VisibleForTesting
        fun forTesting(): ScreenshotData {
            return ScreenshotData(0, 0, null, null, null, 0, Insets.NONE, null)
        }
        fun forTesting() =
            ScreenshotData(
                type = 0,
                source = 0,
                userHandle = null,
                topComponent = null,
                screenBounds = null,
                taskId = 0,
                insets = Insets.NONE,
                bitmap = null,
                displayId = Display.DEFAULT_DISPLAY,
            )
    }
}
+201 −0
Original line number Diff line number Diff line
package com.android.systemui.screenshot

import android.net.Uri
import android.os.Trace
import android.util.Log
import android.view.Display
import com.android.internal.logging.UiEventLogger
import com.android.internal.util.ScreenshotRequest
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
 * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
 * result.
 *
 * Captures a screenshot for each [Display] available.
 */
@SysUISingleton
class TakeScreenshotExecutor
@Inject
constructor(
    private val screenshotControllerFactory: ScreenshotController.Factory,
    displayRepository: DisplayRepository,
    @Application private val mainScope: CoroutineScope,
    private val screenshotRequestProcessor: ScreenshotRequestProcessor,
    private val uiEventLogger: UiEventLogger
) {

    private lateinit var displays: StateFlow<Set<Display>>
    private val displaysCollectionJob: Job =
        mainScope.launch {
            displays = displayRepository.displays.stateIn(this, SharingStarted.Eagerly, emptySet())
        }

    private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()

    /**
     * Executes the [ScreenshotRequest].
     *
     * [onSaved] is invoked only on the default display result. [RequestCallback.onFinish] is
     * invoked only when both screenshot UIs are removed.
     */
    suspend fun executeScreenshots(
        screenshotRequest: ScreenshotRequest,
        onSaved: (Uri) -> Unit,
        requestCallback: RequestCallback
    ) {
        val displayIds = getDisplaysToScreenshot()
        val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
        screenshotRequest.oneForEachDisplay(displayIds).forEach { screenshotData: ScreenshotData ->
            dispatchToController(
                screenshotData = screenshotData,
                onSaved =
                    if (screenshotData.displayId == Display.DEFAULT_DISPLAY) onSaved else { _ -> },
                callback = resultCallbackWrapper.createCallbackForId(screenshotData.displayId)
            )
        }
    }

    /** Creates a [ScreenshotData] for each display. */
    private suspend fun ScreenshotRequest.oneForEachDisplay(
        displayIds: List<Int>
    ): List<ScreenshotData> {
        return displayIds
            .map { displayId -> ScreenshotData.fromRequest(this, displayId) }
            .map { screenshotData: ScreenshotData ->
                screenshotRequestProcessor.process(screenshotData)
            }
    }

    private fun dispatchToController(
        screenshotData: ScreenshotData,
        onSaved: (Uri) -> Unit,
        callback: RequestCallback
    ) {
        uiEventLogger.log(
            ScreenshotEvent.getScreenshotSource(screenshotData.source),
            0,
            screenshotData.packageNameString
        )
        Log.d(TAG, "Screenshot request: $screenshotData")
        getScreenshotController(screenshotData.displayId)
            .handleScreenshot(screenshotData, onSaved, callback)
    }

    private fun getDisplaysToScreenshot(): List<Int> {
        return displays.value.filter { it.type in ALLOWED_DISPLAY_TYPES }.map { it.displayId }
    }

    /**
     * Propagates the close system dialog signal to all controllers.
     *
     * TODO(b/295143676): Move the receiver in this class once the flag is flipped.
     */
    fun onCloseSystemDialogsReceived() {
        screenshotControllers.forEach { (_, screenshotController) ->
            if (!screenshotController.isPendingSharedTransition) {
                screenshotController.dismissScreenshot(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
            }
        }
    }

    /** Removes all screenshot related windows. */
    fun removeWindows() {
        screenshotControllers.forEach { (_, screenshotController) ->
            screenshotController.removeWindow()
        }
    }

    /**
     * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
     */
    fun onDestroy() {
        screenshotControllers.forEach { (_, screenshotController) ->
            screenshotController.onDestroy()
        }
        screenshotControllers.clear()
        displaysCollectionJob.cancel()
    }

    private fun getScreenshotController(id: Int): ScreenshotController {
        return screenshotControllers.computeIfAbsent(id) { screenshotControllerFactory.create(id) }
    }

    /** For java compatibility only. see [executeScreenshots] */
    fun executeScreenshotsAsync(
        screenshotRequest: ScreenshotRequest,
        onSaved: Consumer<Uri>,
        requestCallback: RequestCallback
    ) {
        mainScope.launch {
            executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
        }
    }

    /**
     * Returns a [RequestCallback] that calls [RequestCallback.onFinish] only when all callbacks for
     * id created have finished.
     *
     * If any callback created calls [reportError], then following [onFinish] are not considered.
     */
    private class MultiResultCallbackWrapper(
        private val originalCallback: RequestCallback,
    ) {
        private val idsPending = mutableSetOf<Int>()
        private var errorReported = false

        /**
         * Creates a callback for [id].
         *
         * [originalCallback]'s [onFinish] will be called only when this (and the other created)
         * callback's [onFinish] have been called.
         */
        fun createCallbackForId(id: Int): RequestCallback {
            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TAG, "Waiting for id=$id", id)
            idsPending += id
            return object : RequestCallback {
                override fun reportError() {
                    Log.d(TAG, "ReportError id=$id")
                    Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TAG, id)
                    Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "reportError id=$id")
                    originalCallback.reportError()
                    errorReported = true
                }

                override fun onFinish() {
                    Log.d(TAG, "onFinish id=$id")
                    if (errorReported) return
                    idsPending -= id
                    Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "onFinish id=$id")
                    if (idsPending.isEmpty()) {
                        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TAG, id)
                        originalCallback.onFinish()
                    }
                }
            }
        }
    }

    private companion object {
        val TAG = LogConfig.logTag(TakeScreenshotService::class.java)

        val ALLOWED_DISPLAY_TYPES =
            listOf(
                Display.TYPE_EXTERNAL,
                Display.TYPE_INTERNAL,
                Display.TYPE_OVERLAY,
                Display.TYPE_WIFI
            )
    }
}
+40 −12
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;

import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
import static com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT;
import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
@@ -46,6 +47,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.view.Display;
import android.widget.Toast;

import com.android.internal.annotations.VisibleForTesting;
@@ -59,6 +61,7 @@ import java.util.concurrent.Executor;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Provider;

public class TakeScreenshotService extends Service {
    private static final String TAG = logTag(TakeScreenshotService.class);
@@ -82,12 +85,17 @@ public class TakeScreenshotService extends Service {
                if (DEBUG_DISMISS) {
                    Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
                }
                if (!mScreenshot.isPendingSharedTransition()) {
                if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
                    // TODO(b/295143676): move receiver inside executor when the flag is enabled.
                    mTakeScreenshotExecutor.get().onCloseSystemDialogsReceived();
                } else if (!mScreenshot.isPendingSharedTransition()) {
                    mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER);
                }
            }
        }
    };
    private final Provider<TakeScreenshotExecutor> mTakeScreenshotExecutor;


    /** Informs about coarse grained state of the Controller. */
    public interface RequestCallback {
@@ -99,16 +107,15 @@ public class TakeScreenshotService extends Service {
    }

    @Inject
    public TakeScreenshotService(ScreenshotController screenshotController, UserManager userManager,
            DevicePolicyManager devicePolicyManager, UiEventLogger uiEventLogger,
            ScreenshotNotificationsController notificationsController, Context context,
            @Background Executor bgExecutor, FeatureFlags featureFlags,
            RequestProcessor processor) {
    public TakeScreenshotService(ScreenshotController.Factory screenshotControllerFactory,
            UserManager userManager, DevicePolicyManager devicePolicyManager,
            UiEventLogger uiEventLogger, ScreenshotNotificationsController notificationsController,
            Context context, @Background Executor bgExecutor, FeatureFlags featureFlags,
            RequestProcessor processor, Provider<TakeScreenshotExecutor> takeScreenshotExecutor) {
        if (DEBUG_SERVICE) {
            Log.d(TAG, "new " + this);
        }
        mHandler = new Handler(Looper.getMainLooper(), this::handleMessage);
        mScreenshot = screenshotController;
        mUserManager = userManager;
        mDevicePolicyManager = devicePolicyManager;
        mUiEventLogger = uiEventLogger;
@@ -117,6 +124,12 @@ public class TakeScreenshotService extends Service {
        mBgExecutor = bgExecutor;
        mFeatureFlags = featureFlags;
        mProcessor = processor;
        mTakeScreenshotExecutor = takeScreenshotExecutor;
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mScreenshot = null;
        } else {
            mScreenshot = screenshotControllerFactory.create(Display.DEFAULT_DISPLAY);
        }
    }

    @Override
@@ -142,7 +155,11 @@ public class TakeScreenshotService extends Service {
        if (DEBUG_SERVICE) {
            Log.d(TAG, "onUnbind");
        }
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mTakeScreenshotExecutor.get().removeWindows();
        } else {
            mScreenshot.removeWindow();
        }
        unregisterReceiver(mCloseSystemDialogs);
        return false;
    }
@@ -150,7 +167,11 @@ public class TakeScreenshotService extends Service {
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
            mTakeScreenshotExecutor.get().onDestroy();
        } else {
            mScreenshot.onDestroy();
        }
        if (DEBUG_SERVICE) {
            Log.d(TAG, "onDestroy");
        }
@@ -218,10 +239,17 @@ public class TakeScreenshotService extends Service {
        }

        Log.d(TAG, "Processing screenshot data");
        ScreenshotData screenshotData = ScreenshotData.fromRequest(request);


        ScreenshotData screenshotData = ScreenshotData.fromRequest(
                request, Display.DEFAULT_DISPLAY);
        try {
            mProcessor.processAsync(screenshotData,
                    (data) -> dispatchToController(data, onSaved, callback));
            if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
                mTakeScreenshotExecutor.get().executeScreenshotsAsync(request, onSaved, callback);
            } else {
                mProcessor.processAsync(screenshotData, (data) ->
                        dispatchToController(data, onSaved, callback));
            }
        } catch (IllegalStateException e) {
            Log.e(TAG, "Failed to process screenshot request!", e);
            logFailedRequest(request);
Loading