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

Commit fe1006fb authored by Nicolò Mazzucato's avatar Nicolò Mazzucato
Browse files

Refactor screenshot logging in case of external display

This makes a few changes to make error logging more explicit in case of
connected displays.

- RequestProcessor now throws a typed exception, so we can avoid
catching unrelated errors in TakeScreenshotExecutor
- ScreenshotNotificationsController can be created only with a factory,
providing a displayId. The display ID is used to append "(External
Display)" string for error notifications happening on any display that
is not the default one.
- There can be a maximum of 2 error notifications: one for the default
display (as before), and another that is shown in case of any error in
any external display connected.
- Error reporting for ScreenshotData processing or ScreenshotController
issues has been completely moved to TakeScreenshotExecutor and scoped to
 display ID. TakeScreenshotService still reports some early failures
 unrelated to displays.
- As we can't specify the displayId in UiEventLogger: if the failure is
generic and we reach it from TakescreenshotService, only one
SCREENSHOT_REQUESTED_... is reported. However, if the screenshot request
 reaches TakeScreenshotExecutor, then one event per display will be
 reported.

Test: RequestProcessorTest, TakeScreenshotExecutorTest, TakeScreenshotServiceTest
Bug: 296575569
Bug: 290910794
Change-Id: If187e9713b344605466a2dcb78267ededccfcc85
parent a10767e3
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -209,6 +209,8 @@
    <string name="screenshot_saved_title">Screenshot saved</string>
    <!-- Notification title displayed when we fail to take a screenshot. [CHAR LIMIT=50] -->
    <string name="screenshot_failed_title">Couldn\'t save screenshot</string>
    <!-- Appended to the notification content when a screenshot failure happens on an external display. [CHAR LIMIT=50] -->
    <string name="screenshot_failed_external_display_indication">External Display</string>
    <!-- Notification text displayed when we fail to save a screenshot due to locked storage. [CHAR LIMIT=100] -->
    <string name="screenshot_failed_to_save_user_locked_text">Device must be unlocked before screenshot can be saved</string>
    <!-- Notification text displayed when we fail to save a screenshot for unknown reasons. [CHAR LIMIT=100] -->
+16 −14
Original line number Diff line number Diff line
@@ -20,11 +20,10 @@ import android.util.Log
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/** Processes a screenshot request sent from [ScreenshotHelper]. */
interface ScreenshotRequestProcessor {
@@ -36,14 +35,13 @@ interface ScreenshotRequestProcessor {
    suspend fun process(screenshot: ScreenshotData): ScreenshotData
}

/**
 * Implementation of [ScreenshotRequestProcessor]
 */
/** Implementation of [ScreenshotRequestProcessor] */
@SysUISingleton
class RequestProcessor @Inject constructor(
class RequestProcessor
@Inject
constructor(
    private val capture: ImageCapture,
    private val policy: ScreenshotPolicy,
        private val flags: FeatureFlags,
    /** For the Java Async version, to invoke the callback. */
    @Application private val mainScope: CoroutineScope
) : ScreenshotRequestProcessor {
@@ -67,8 +65,9 @@ class RequestProcessor @Inject constructor(
            result.userHandle = info.user

            if (policy.isManagedProfile(info.user.identifier)) {
                val image = capture.captureTask(info.taskId)
                    ?: error("Task snapshot returned a null Bitmap!")
                val image =
                    capture.captureTask(info.taskId)
                        ?: throw RequestProcessorException("Task snapshot returned a null Bitmap!")

                // Provide the task snapshot as the screenshot
                result.type = TAKE_SCREENSHOT_PROVIDED_IMAGE
@@ -97,3 +96,6 @@ class RequestProcessor @Inject constructor(
}

private const val TAG = "RequestProcessor"

/** Exception thrown by [RequestProcessor] if something goes wrong. */
class RequestProcessorException(message: String) : IllegalStateException(message)
+2 −2
Original line number Diff line number Diff line
@@ -325,7 +325,7 @@ public class ScreenshotController {
            Context context,
            FeatureFlags flags,
            ScreenshotSmartActions screenshotSmartActions,
            ScreenshotNotificationsController screenshotNotificationsController,
            ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
            ScrollCaptureClient scrollCaptureClient,
            UiEventLogger uiEventLogger,
            ImageExporter imageExporter,
@@ -346,7 +346,7 @@ public class ScreenshotController {
            @Assisted boolean showUIOnExternalDisplay
    ) {
        mScreenshotSmartActions = screenshotSmartActions;
        mNotificationsController = screenshotNotificationsController;
        mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
        mScrollCaptureClient = scrollCaptureClient;
        mUiEventLogger = uiEventLogger;
        mImageExporter = imageExporter;
+0 −96
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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 static android.content.Context.NOTIFICATION_SERVICE;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.UserHandle;
import android.util.DisplayMetrics;
import android.view.WindowManager;

import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.res.R;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.util.NotificationChannels;

import javax.inject.Inject;

/**
 * Convenience class to handle showing and hiding notifications while taking a screenshot.
 */
public class ScreenshotNotificationsController {
    private static final String TAG = "ScreenshotNotificationManager";

    private final Context mContext;
    private final Resources mResources;
    private final NotificationManager mNotificationManager;

    @Inject
    ScreenshotNotificationsController(Context context, WindowManager windowManager) {
        mContext = context;
        mResources = context.getResources();
        mNotificationManager =
                (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);

        DisplayMetrics displayMetrics = new DisplayMetrics();
        windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
    }

    /**
     * Sends a notification that the screenshot capture has failed.
     */
    public void notifyScreenshotError(int msgResId) {
        Resources res = mContext.getResources();
        String errorMsg = res.getString(msgResId);

        // Repurpose the existing notification to notify the user of the error
        Notification.Builder b = new Notification.Builder(mContext, NotificationChannels.ALERTS)
                .setTicker(res.getString(R.string.screenshot_failed_title))
                .setContentTitle(res.getString(R.string.screenshot_failed_title))
                .setContentText(errorMsg)
                .setSmallIcon(R.drawable.stat_notify_image_error)
                .setWhen(System.currentTimeMillis())
                .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
                .setCategory(Notification.CATEGORY_ERROR)
                .setAutoCancel(true)
                .setColor(mContext.getColor(
                        com.android.internal.R.color.system_notification_accent_color));
        final DevicePolicyManager dpm =
                (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
        final Intent intent =
                dpm.createAdminSupportIntent(DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE);
        if (intent != null) {
            final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                    mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT);
            b.setContentIntent(pendingIntent);
        }

        SystemUIApplication.overrideNotificationAppName(mContext, b, true);

        Notification n = new Notification.BigTextStyle(b)
                .bigText(errorMsg)
                .build();
        mNotificationManager.notify(SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT, n);
    }
}
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.os.UserHandle
import android.view.Display
import com.android.internal.R
import com.android.internal.messages.nano.SystemMessageProto
import com.android.systemui.SystemUIApplication
import com.android.systemui.util.NotificationChannels
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/** Convenience class to handle showing and hiding notifications while taking a screenshot. */
class ScreenshotNotificationsController
@AssistedInject
internal constructor(
    @Assisted private val displayId: Int,
    private val context: Context,
    private val notificationManager: NotificationManager,
    private val devicePolicyManager: DevicePolicyManager,
) {
    private val res = context.resources

    /**
     * Sends a notification that the screenshot capture has failed.
     *
     * Errors for the non-default display are shown in a unique separate notification.
     */
    fun notifyScreenshotError(msgResId: Int) {
        val displayErrorString =
            if (displayId != Display.DEFAULT_DISPLAY) {
                " ($externalDisplayString)"
            } else {
                ""
            }
        val errorMsg = res.getString(msgResId) + displayErrorString

        // Repurpose the existing notification or create a new one
        val builder =
            Notification.Builder(context, NotificationChannels.ALERTS)
                .setTicker(res.getString(com.android.systemui.res.R.string.screenshot_failed_title))
                .setContentTitle(
                    res.getString(com.android.systemui.res.R.string.screenshot_failed_title)
                )
                .setContentText(errorMsg)
                .setSmallIcon(com.android.systemui.res.R.drawable.stat_notify_image_error)
                .setWhen(System.currentTimeMillis())
                .setVisibility(Notification.VISIBILITY_PUBLIC) // ok to show outside lockscreen
                .setCategory(Notification.CATEGORY_ERROR)
                .setAutoCancel(true)
                .setColor(context.getColor(R.color.system_notification_accent_color))
        val intent =
            devicePolicyManager.createAdminSupportIntent(
                DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE
            )
        if (intent != null) {
            val pendingIntent =
                PendingIntent.getActivityAsUser(
                    context,
                    0,
                    intent,
                    PendingIntent.FLAG_IMMUTABLE,
                    null,
                    UserHandle.CURRENT
                )
            builder.setContentIntent(pendingIntent)
        }
        SystemUIApplication.overrideNotificationAppName(context, builder, true)
        val notification = Notification.BigTextStyle(builder).bigText(errorMsg).build()
        // A different id for external displays to keep the 2 error notifications separated.
        val id =
            if (displayId == Display.DEFAULT_DISPLAY) {
                SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT
            } else {
                SystemMessageProto.SystemMessage.NOTE_GLOBAL_SCREENSHOT_EXTERNAL_DISPLAY
            }
        notificationManager.notify(id, notification)
    }

    private val externalDisplayString: String
        get() =
            res.getString(
                com.android.systemui.res.R.string.screenshot_failed_external_display_indication
            )

    /** Factory for [ScreenshotNotificationsController]. */
    @AssistedFactory
    interface Factory {
        fun create(displayId: Int = Display.DEFAULT_DISPLAY): ScreenshotNotificationsController
    }
}
Loading