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

Commit 31ab31b0 authored by Matt Casey's avatar Matt Casey
Browse files

Make non-main displays take headless screenshots.

Instead of expecting ScreenshotController to function headlessly,
separate the small bit of code that does the actual saving into its own
class and use that instead of ScreenshotController when headless
operation is needed for secondary displays.

Existing "no-UI" functionality in ScreenshotController is untouched for
now to minimize the scope of the change.

Bug: 339424226
Test: atest TakeScreenshotExecutorTest
Test: manual testing with secondary display.
Flag: EXEMPT bugfix
Change-Id: I3abc3e72f43649e114358ab7aa6d14e807fc3bf2
parent fcf5c037
Loading
Loading
Loading
Loading
+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"
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -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);

    /**
@@ -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();

+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
@@ -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
@@ -26,9 +42,13 @@ interface TakeScreenshotExecutor {
        onSaved: (Uri?) -> Unit,
        requestCallback: RequestCallback
    )

    fun onCloseSystemDialogsReceived()

    fun removeWindows()

    fun onDestroy()

    fun executeScreenshotsAsync(
        screenshotRequest: ScreenshotRequest,
        onSaved: Consumer<Uri?>,
@@ -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.
@@ -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>()

    /**
@@ -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) {
@@ -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
@@ -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)
@@ -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 {
@@ -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].
     *
+37 −88

File changed.

Preview size limit exceeded, changes collapsed.