Loading packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt 0 → 100644 +114 −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.net.Uri import android.os.UserManager import android.util.Log import android.view.WindowManager import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.google.common.util.concurrent.ListenableFuture import java.util.UUID import java.util.concurrent.Executor import java.util.concurrent.Executors import java.util.function.Consumer import javax.inject.Inject /** * A ScreenshotHandler that just saves the screenshot and calls back as appropriate, with no UI. * * Basically, ScreenshotController with all the UI bits ripped out. */ class HeadlessScreenshotHandler @Inject constructor( private val imageExporter: ImageExporter, @Main private val mainExecutor: Executor, private val imageCapture: ImageCapture, private val userManager: UserManager, private val uiEventLogger: UiEventLogger, private val notificationsControllerFactory: ScreenshotNotificationsController.Factory, ) : ScreenshotHandler { override fun handleScreenshot( screenshot: ScreenshotData, finisher: Consumer<Uri?>, requestCallback: TakeScreenshotService.RequestCallback ) { if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null) } if (screenshot.bitmap == null) { Log.e(TAG, "handleScreenshot: Screenshot bitmap was null") notificationsControllerFactory .create(screenshot.displayId) .notifyScreenshotError(R.string.screenshot_failed_to_capture_text) requestCallback.reportError() return } val future: ListenableFuture<ImageExporter.Result> = imageExporter.export( Executors.newSingleThreadExecutor(), UUID.randomUUID(), screenshot.bitmap, screenshot.getUserOrDefault(), screenshot.displayId ) future.addListener( { try { val result = future.get() Log.d(TAG, "Saved screenshot: $result") logScreenshotResultStatus(result.uri, screenshot) finisher.accept(result.uri) requestCallback.onFinish() } catch (e: Exception) { Log.d(TAG, "Failed to store screenshot", e) finisher.accept(null) requestCallback.reportError() } }, mainExecutor ) } private fun logScreenshotResultStatus(uri: Uri?, screenshot: ScreenshotData) { if (uri == null) { uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, screenshot.packageNameString) notificationsControllerFactory .create(screenshot.displayId) .notifyScreenshotError(R.string.screenshot_failed_to_save_text) } else { uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString) if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) { uiEventLogger.log( ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, screenshot.packageNameString ) } } } companion object { const val TAG = "HeadlessScreenshotHandler" } } packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +3 −2 Original line number Diff line number Diff line Loading @@ -101,7 +101,7 @@ import javax.inject.Provider; /** * Controls the state and flow for screenshots. */ public class ScreenshotController { public class ScreenshotController implements ScreenshotHandler { private static final String TAG = logTag(ScreenshotController.class); /** Loading Loading @@ -351,7 +351,8 @@ public class ScreenshotController { mShowUIOnExternalDisplay = showUIOnExternalDisplay; } void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, @Override public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback) { Assert.isMainThread(); Loading packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +55 −28 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.net.Uri Loading @@ -7,12 +23,12 @@ import android.view.Display import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE import com.android.internal.logging.UiEventLogger import com.android.internal.util.ScreenshotRequest import com.android.systemui.Flags.screenshotShelfUi2 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.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback import java.util.function.Consumer import javax.inject.Inject Loading @@ -26,9 +42,13 @@ interface TakeScreenshotExecutor { onSaved: (Uri?) -> Unit, requestCallback: RequestCallback ) fun onCloseSystemDialogsReceived() fun removeWindows() fun onDestroy() fun executeScreenshotsAsync( screenshotRequest: ScreenshotRequest, onSaved: Consumer<Uri?>, Loading @@ -36,6 +56,14 @@ interface TakeScreenshotExecutor { ) } interface ScreenshotHandler { fun handleScreenshot( screenshot: ScreenshotData, finisher: Consumer<Uri?>, requestCallback: RequestCallback ) } /** * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the * result. Loading @@ -52,10 +80,10 @@ constructor( private val screenshotRequestProcessor: ScreenshotRequestProcessor, private val uiEventLogger: UiEventLogger, private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory, private val headlessScreenshotHandler: HeadlessScreenshotHandler, ) : TakeScreenshotExecutor { private val displays = displayRepository.displays private val screenshotControllers = mutableMapOf<Int, ScreenshotController>() private var screenshotController: ScreenshotController? = null private val notificationControllers = mutableMapOf<Int, ScreenshotNotificationsController>() /** Loading @@ -73,9 +101,15 @@ constructor( val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback) displays.forEach { display -> val displayId = display.displayId var screenshotHandler: ScreenshotHandler = if (displayId == Display.DEFAULT_DISPLAY) { getScreenshotController(display) } else { headlessScreenshotHandler } Log.d(TAG, "Executing screenshot for display $displayId") dispatchToController( display = display, screenshotHandler, rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId), onSaved = if (displayId == Display.DEFAULT_DISPLAY) { Loading @@ -88,7 +122,7 @@ constructor( /** All logging should be triggered only by this method. */ private suspend fun dispatchToController( display: Display, screenshotHandler: ScreenshotHandler, rawScreenshotData: ScreenshotData, onSaved: (Uri?) -> Unit, callback: RequestCallback Loading @@ -102,13 +136,12 @@ constructor( logScreenshotRequested(rawScreenshotData) onFailedScreenshotRequest(rawScreenshotData, callback) } .getOrNull() ?: return .getOrNull() ?: return logScreenshotRequested(screenshotData) Log.d(TAG, "Screenshot request: $screenshotData") try { getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback) screenshotHandler.handleScreenshot(screenshotData, onSaved, callback) } catch (e: IllegalStateException) { Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e) onFailedScreenshotRequest(screenshotData, callback) Loading Loading @@ -140,44 +173,32 @@ constructor( private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> { val allDisplays = displays.first() return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) { // If this is a provided image or using the shelf UI, just screenshot th default display return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) { // If this is a provided image just screenshot th default display allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY } } else { allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES } } } /** Propagates the close system dialog signal to all controllers. */ /** Propagates the close system dialog signal to the ScreenshotController. */ override fun onCloseSystemDialogsReceived() { screenshotControllers.forEach { (_, screenshotController) -> if (!screenshotController.isPendingSharedTransition) { screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) } if (screenshotController?.isPendingSharedTransition == false) { screenshotController?.requestDismissal(SCREENSHOT_DISMISSED_OTHER) } } /** Removes all screenshot related windows. */ override fun removeWindows() { screenshotControllers.forEach { (_, screenshotController) -> screenshotController.removeWindow() } screenshotController?.removeWindow() } /** * Destroys the executor. Afterwards, this class is not expected to work as intended anymore. */ override fun onDestroy() { screenshotControllers.forEach { (_, screenshotController) -> screenshotController.onDestroy() } screenshotControllers.clear() } private fun getScreenshotController(display: Display): ScreenshotController { return screenshotControllers.computeIfAbsent(display.displayId) { screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false) } screenshotController?.onDestroy() screenshotController = null } private fun getNotificationController(id: Int): ScreenshotNotificationsController { Loading @@ -197,6 +218,12 @@ constructor( } } private fun getScreenshotController(display: Display): ScreenshotController { val controller = screenshotController ?: screenshotControllerFactory.create(display, false) screenshotController = controller return controller } /** * Returns a [RequestCallback] that wraps [originalCallback]. * Loading packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +37 −88 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt 0 → 100644 +114 −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.net.Uri import android.os.UserManager import android.util.Log import android.view.WindowManager import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.google.common.util.concurrent.ListenableFuture import java.util.UUID import java.util.concurrent.Executor import java.util.concurrent.Executors import java.util.function.Consumer import javax.inject.Inject /** * A ScreenshotHandler that just saves the screenshot and calls back as appropriate, with no UI. * * Basically, ScreenshotController with all the UI bits ripped out. */ class HeadlessScreenshotHandler @Inject constructor( private val imageExporter: ImageExporter, @Main private val mainExecutor: Executor, private val imageCapture: ImageCapture, private val userManager: UserManager, private val uiEventLogger: UiEventLogger, private val notificationsControllerFactory: ScreenshotNotificationsController.Factory, ) : ScreenshotHandler { override fun handleScreenshot( screenshot: ScreenshotData, finisher: Consumer<Uri?>, requestCallback: TakeScreenshotService.RequestCallback ) { if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null) } if (screenshot.bitmap == null) { Log.e(TAG, "handleScreenshot: Screenshot bitmap was null") notificationsControllerFactory .create(screenshot.displayId) .notifyScreenshotError(R.string.screenshot_failed_to_capture_text) requestCallback.reportError() return } val future: ListenableFuture<ImageExporter.Result> = imageExporter.export( Executors.newSingleThreadExecutor(), UUID.randomUUID(), screenshot.bitmap, screenshot.getUserOrDefault(), screenshot.displayId ) future.addListener( { try { val result = future.get() Log.d(TAG, "Saved screenshot: $result") logScreenshotResultStatus(result.uri, screenshot) finisher.accept(result.uri) requestCallback.onFinish() } catch (e: Exception) { Log.d(TAG, "Failed to store screenshot", e) finisher.accept(null) requestCallback.reportError() } }, mainExecutor ) } private fun logScreenshotResultStatus(uri: Uri?, screenshot: ScreenshotData) { if (uri == null) { uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, screenshot.packageNameString) notificationsControllerFactory .create(screenshot.displayId) .notifyScreenshotError(R.string.screenshot_failed_to_save_text) } else { uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString) if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) { uiEventLogger.log( ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, screenshot.packageNameString ) } } } companion object { const val TAG = "HeadlessScreenshotHandler" } }
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +3 −2 Original line number Diff line number Diff line Loading @@ -101,7 +101,7 @@ import javax.inject.Provider; /** * Controls the state and flow for screenshots. */ public class ScreenshotController { public class ScreenshotController implements ScreenshotHandler { private static final String TAG = logTag(ScreenshotController.class); /** Loading Loading @@ -351,7 +351,8 @@ public class ScreenshotController { mShowUIOnExternalDisplay = showUIOnExternalDisplay; } void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, @Override public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback) { Assert.isMainThread(); Loading
packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +55 −28 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.net.Uri Loading @@ -7,12 +23,12 @@ import android.view.Display import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE import com.android.internal.logging.UiEventLogger import com.android.internal.util.ScreenshotRequest import com.android.systemui.Flags.screenshotShelfUi2 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.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback import java.util.function.Consumer import javax.inject.Inject Loading @@ -26,9 +42,13 @@ interface TakeScreenshotExecutor { onSaved: (Uri?) -> Unit, requestCallback: RequestCallback ) fun onCloseSystemDialogsReceived() fun removeWindows() fun onDestroy() fun executeScreenshotsAsync( screenshotRequest: ScreenshotRequest, onSaved: Consumer<Uri?>, Loading @@ -36,6 +56,14 @@ interface TakeScreenshotExecutor { ) } interface ScreenshotHandler { fun handleScreenshot( screenshot: ScreenshotData, finisher: Consumer<Uri?>, requestCallback: RequestCallback ) } /** * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the * result. Loading @@ -52,10 +80,10 @@ constructor( private val screenshotRequestProcessor: ScreenshotRequestProcessor, private val uiEventLogger: UiEventLogger, private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory, private val headlessScreenshotHandler: HeadlessScreenshotHandler, ) : TakeScreenshotExecutor { private val displays = displayRepository.displays private val screenshotControllers = mutableMapOf<Int, ScreenshotController>() private var screenshotController: ScreenshotController? = null private val notificationControllers = mutableMapOf<Int, ScreenshotNotificationsController>() /** Loading @@ -73,9 +101,15 @@ constructor( val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback) displays.forEach { display -> val displayId = display.displayId var screenshotHandler: ScreenshotHandler = if (displayId == Display.DEFAULT_DISPLAY) { getScreenshotController(display) } else { headlessScreenshotHandler } Log.d(TAG, "Executing screenshot for display $displayId") dispatchToController( display = display, screenshotHandler, rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId), onSaved = if (displayId == Display.DEFAULT_DISPLAY) { Loading @@ -88,7 +122,7 @@ constructor( /** All logging should be triggered only by this method. */ private suspend fun dispatchToController( display: Display, screenshotHandler: ScreenshotHandler, rawScreenshotData: ScreenshotData, onSaved: (Uri?) -> Unit, callback: RequestCallback Loading @@ -102,13 +136,12 @@ constructor( logScreenshotRequested(rawScreenshotData) onFailedScreenshotRequest(rawScreenshotData, callback) } .getOrNull() ?: return .getOrNull() ?: return logScreenshotRequested(screenshotData) Log.d(TAG, "Screenshot request: $screenshotData") try { getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback) screenshotHandler.handleScreenshot(screenshotData, onSaved, callback) } catch (e: IllegalStateException) { Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e) onFailedScreenshotRequest(screenshotData, callback) Loading Loading @@ -140,44 +173,32 @@ constructor( private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> { val allDisplays = displays.first() return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) { // If this is a provided image or using the shelf UI, just screenshot th default display return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) { // If this is a provided image just screenshot th default display allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY } } else { allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES } } } /** Propagates the close system dialog signal to all controllers. */ /** Propagates the close system dialog signal to the ScreenshotController. */ override fun onCloseSystemDialogsReceived() { screenshotControllers.forEach { (_, screenshotController) -> if (!screenshotController.isPendingSharedTransition) { screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) } if (screenshotController?.isPendingSharedTransition == false) { screenshotController?.requestDismissal(SCREENSHOT_DISMISSED_OTHER) } } /** Removes all screenshot related windows. */ override fun removeWindows() { screenshotControllers.forEach { (_, screenshotController) -> screenshotController.removeWindow() } screenshotController?.removeWindow() } /** * Destroys the executor. Afterwards, this class is not expected to work as intended anymore. */ override fun onDestroy() { screenshotControllers.forEach { (_, screenshotController) -> screenshotController.onDestroy() } screenshotControllers.clear() } private fun getScreenshotController(display: Display): ScreenshotController { return screenshotControllers.computeIfAbsent(display.displayId) { screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false) } screenshotController?.onDestroy() screenshotController = null } private fun getNotificationController(id: Int): ScreenshotNotificationsController { Loading @@ -197,6 +218,12 @@ constructor( } } private fun getScreenshotController(display: Display): ScreenshotController { val controller = screenshotController ?: screenshotControllerFactory.create(display, false) screenshotController = controller return controller } /** * Returns a [RequestCallback] that wraps [originalCallback]. * Loading
packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +37 −88 File changed.Preview size limit exceeded, changes collapsed. Show changes