Loading packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +14 −10 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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. * Loading @@ -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") Loading Loading @@ -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: Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +27 −18 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -314,7 +316,8 @@ public class ScreenshotController { | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS); @Inject @AssistedInject ScreenshotController( Context context, FeatureFlags flags, Loading @@ -335,7 +338,7 @@ public class ScreenshotController { UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, DisplayTracker displayTracker @Assisted int displayId ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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) -> { Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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); } Loading Loading @@ -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() { Loading @@ -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); } Loading Loading @@ -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); } } packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +26 −15 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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, ) { Loading @@ -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, ) } } packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt 0 → 100644 +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 ) } } packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +40 −12 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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 { Loading @@ -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; Loading @@ -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 Loading @@ -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; } Loading @@ -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"); } Loading Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +14 −10 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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. * Loading @@ -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") Loading Loading @@ -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: Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +27 −18 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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; Loading Loading @@ -314,7 +316,8 @@ public class ScreenshotController { | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS); @Inject @AssistedInject ScreenshotController( Context context, FeatureFlags flags, Loading @@ -335,7 +338,7 @@ public class ScreenshotController { UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, DisplayTracker displayTracker @Assisted int displayId ) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; Loading @@ -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; Loading Loading @@ -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); } Loading Loading @@ -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) -> { Loading Loading @@ -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); Loading Loading @@ -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, Loading Loading @@ -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); } Loading Loading @@ -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() { Loading @@ -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); } Loading Loading @@ -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); } }
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +26 −15 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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, ) { Loading @@ -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, ) } }
packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt 0 → 100644 +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 ) } }
packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +40 −12 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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); Loading @@ -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 { Loading @@ -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; Loading @@ -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 Loading @@ -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; } Loading @@ -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"); } Loading Loading @@ -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