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

Commit 7d949845 authored by Matt Casey's avatar Matt Casey
Browse files

Move smart actions support code.

Not exercised in AOSP, simplify the AOSP ScreenshotActionsProvider to
just supply the supported actions.

Bug: 329659738
Test: atest DefaultScreenshotActionsProviderTest
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT
Change-Id: I9799b8e4312e8d834f50edb2317690aac787dc9d
Merged-In: I9799b8e4312e8d834f50edb2317690aac787dc9d
parent 089280f5
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.screenshot;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;

@@ -29,4 +30,9 @@ public interface ReferenceScreenshotModule {
    static ScreenshotNotificationSmartActionsProvider providesScrnshtNotifSmartActionsProvider() {
        return new ScreenshotNotificationSmartActionsProvider();
    }

    /** */
    @Binds
    ScreenshotActionsProvider.Factory bindScreenshotActionsProviderFactory(
            DefaultScreenshotActionsProvider.Factory defaultScreenshotActionsProviderFactory);
}
+0 −48
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import com.android.systemui.screenshot.ActionIntentCreator.createShareWithSubjec
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED
import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import dagger.assisted.Assisted
@@ -59,7 +58,6 @@ class DefaultScreenshotActionsProvider
constructor(
    private val context: Context,
    private val viewModel: ScreenshotViewModel,
    private val smartActionsProvider: SmartActionsProvider,
    private val uiEventLogger: UiEventLogger,
    @Assisted val request: ScreenshotData,
    @Assisted val requestId: String,
@@ -115,37 +113,6 @@ constructor(
                )
            }
        }

        smartActionsProvider.requestQuickShare(request, requestId) { quickShare ->
            if (!quickShare.actionIntent.isImmutable) {
                viewModel.addAction(
                    ActionButtonAppearance(
                        quickShare.getIcon().loadDrawable(context),
                        quickShare.title,
                        quickShare.title
                    )
                ) {
                    debugLog(LogConfig.DEBUG_ACTIONS) { "Quickshare tapped" }
                    onDeferrableActionTapped { result ->
                        uiEventLogger.log(
                            SCREENSHOT_SMART_ACTION_TAPPED,
                            0,
                            request.packageNameString
                        )
                        val pendingIntentWithUri =
                            smartActionsProvider.wrapIntent(
                                quickShare,
                                result.uri,
                                result.subject,
                                requestId
                            )
                        actionExecutor.sendPendingIntent(pendingIntentWithUri)
                    }
                }
            } else {
                Log.w(TAG, "Received immutable quick share pending intent; ignoring")
            }
        }
    }

    override fun onScrollChipReady(onClick: Runnable) {
@@ -167,21 +134,6 @@ constructor(
        }
        this.result = result
        pendingAction?.invoke(result)
        smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions ->
            smartActions.forEach {
                smartActions.forEach { action ->
                    viewModel.addAction(
                        ActionButtonAppearance(
                            action.getIcon().loadDrawable(context),
                            action.title,
                            action.title,
                        )
                    ) {
                        actionExecutor.sendPendingIntent(action.actionIntent)
                    }
                }
            }
        }
    }

    private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) {
+6 −5
Original line number Diff line number Diff line
@@ -176,11 +176,12 @@ public class ScreenshotController {

    // These strings are used for communicating the action invoked to
    // ScreenshotNotificationSmartActionsProvider.
    static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
    static final String EXTRA_ID = "android:screenshot_id";
    static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
    static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
    static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin";
    public static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
    public static final String EXTRA_ID = "android:screenshot_id";
    public static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
    public static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
    public static final String EXTRA_ACTION_INTENT_FILLIN =
            "android:screenshot_action_intent_fillin";


    // From WizardManagerHelper.java
+2 −2
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ public class ScreenshotNotificationSmartActionsProvider {
    public static final String DEFAULT_ACTION_TYPE = "Smart Action";

    /* Define phases of screenshot execution. */
    protected enum ScreenshotOp {
    public enum ScreenshotOp {
        OP_UNKNOWN,
        RETRIEVE_SMART_ACTIONS,
        REQUEST_SMART_ACTIONS,
@@ -52,7 +52,7 @@ public class ScreenshotNotificationSmartActionsProvider {
    }

    /* Enum to report success or failure for screenshot execution phases. */
    protected enum ScreenshotOpStatus {
    public enum ScreenshotOpStatus {
        OP_STATUS_UNKNOWN,
        SUCCESS,
        ERROR,
+0 −285
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.app.Notification
import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipDescription
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.os.SystemClock
import android.os.UserHandle
import android.provider.DeviceConfig
import android.util.Log
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.systemui.log.DebugLogger.debugLog
import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import kotlin.random.Random

/**
 * Handle requesting smart/quickshare actions from the provider and executing an action when the
 * action futures complete.
 */
class SmartActionsProvider
@Inject
constructor(
    private val context: Context,
    private val smartActions: ScreenshotNotificationSmartActionsProvider,
) {
    /**
     * Requests quick share action for a given screenshot.
     *
     * @param data the ScreenshotData request
     * @param id the request id for the screenshot
     * @param onAction callback to run when quick share action is returned
     */
    fun requestQuickShare(
        data: ScreenshotData,
        id: String,
        onAction: (Notification.Action) -> Unit
    ) {
        val bitmap = data.bitmap ?: return
        val component = data.topComponent ?: ComponentName("", "")
        requestQuickShareAction(id, bitmap, component, data.getUserOrDefault()) { quickShare ->
            onAction(quickShare)
        }
    }

    /**
     * Requests smart actions for a given screenshot.
     *
     * @param data the ScreenshotData request
     * @param id the request id for the screenshot
     * @param result the data for the saved image
     * @param onActions callback to run when actions are returned
     */
    fun requestSmartActions(
        data: ScreenshotData,
        id: String,
        result: ScreenshotSavedResult,
        onActions: (List<Notification.Action>) -> Unit
    ) {
        val bitmap = data.bitmap ?: return
        val component = data.topComponent ?: ComponentName("", "")
        requestSmartActions(
            id,
            bitmap,
            component,
            data.getUserOrDefault(),
            result.uri,
            REGULAR_SMART_ACTIONS
        ) { actions ->
            onActions(actions)
        }
    }

    /**
     * Wraps the given quick share action in a broadcast intent.
     *
     * @param quickShare the quick share action to wrap
     * @param uri the URI of the saved screenshot
     * @param subject the subject/title for the screenshot
     * @param id the request ID of the screenshot
     * @return the pending intent with correct URI
     */
    fun wrapIntent(
        quickShare: Notification.Action,
        uri: Uri,
        subject: String,
        id: String
    ): PendingIntent {
        val wrappedIntent: Intent =
            Intent(context, SmartActionsReceiver::class.java)
                .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
                .putExtra(
                    ScreenshotController.EXTRA_ACTION_INTENT_FILLIN,
                    createFillInIntent(uri, subject)
                )
                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
        val extras: Bundle = quickShare.extras
        val actionType =
            extras.getString(
                ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
                ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE
            )
        // We only query for quick share actions when smart actions are enabled, so we can assert
        // that it's true here.
        wrappedIntent
            .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType)
            .putExtra(ScreenshotController.EXTRA_ID, id)
            .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, true)
        return PendingIntent.getBroadcast(
            context,
            Random.nextInt(),
            wrappedIntent,
            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    private fun createFillInIntent(uri: Uri, subject: String): Intent {
        val fillIn = Intent()
        fillIn.setType("image/png")
        fillIn.putExtra(Intent.EXTRA_STREAM, uri)
        fillIn.putExtra(Intent.EXTRA_SUBJECT, subject)
        // Include URI in ClipData also, so that grantPermission picks it up.
        // We don't use setData here because some apps interpret this as "to:".
        val clipData =
            ClipData(ClipDescription("content", arrayOf("image/png")), ClipData.Item(uri))
        fillIn.clipData = clipData
        fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        return fillIn
    }

    private fun requestQuickShareAction(
        id: String,
        image: Bitmap,
        component: ComponentName,
        user: UserHandle,
        timeoutMs: Long = 500,
        onAction: (Notification.Action) -> Unit
    ) {
        requestSmartActions(id, image, component, user, null, QUICK_SHARE_ACTION, timeoutMs) {
            it.firstOrNull()?.let { action -> onAction(action) }
        }
    }

    private fun requestSmartActions(
        id: String,
        image: Bitmap,
        component: ComponentName,
        user: UserHandle,
        uri: Uri?,
        actionType: ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType,
        timeoutMs: Long = 500,
        onActions: (List<Notification.Action>) -> Unit
    ) {
        val enabled = isSmartActionsEnabled(user)
        debugLog(DEBUG_ACTIONS) {
            ("getSmartActionsFuture id=$id, uri=$uri, provider=$smartActions, " +
                "actionType=$actionType, smartActionsEnabled=$enabled, userHandle=$user")
        }
        if (!enabled) {
            debugLog(DEBUG_ACTIONS) { "Screenshot Intelligence not enabled, returning empty list" }
            onActions(listOf())
            return
        }
        if (image.config != Bitmap.Config.HARDWARE) {
            debugLog(DEBUG_ACTIONS) {
                "Bitmap expected: Hardware, Bitmap found: ${image.config}. Returning empty list."
            }
            onActions(listOf())
            return
        }
        val smartActionsFuture: CompletableFuture<List<Notification.Action>>
        val startTimeMs = SystemClock.uptimeMillis()
        try {
            smartActionsFuture =
                smartActions.getActions(id, uri, image, component, actionType, user)
        } catch (e: Throwable) {
            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
            debugLog(DEBUG_ACTIONS, error = e) {
                "Failed to get future for screenshot notification smart actions."
            }
            notifyScreenshotOp(
                id,
                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.REQUEST_SMART_ACTIONS,
                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR,
                waitTimeMs
            )
            onActions(listOf())
            return
        }
        try {
            val actions = smartActionsFuture.get(timeoutMs, TimeUnit.MILLISECONDS)
            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
            debugLog(DEBUG_ACTIONS) {
                ("Got ${actions.size} smart actions. Wait time: $waitTimeMs ms, " +
                    "actionType=$actionType")
            }
            notifyScreenshotOp(
                id,
                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
                waitTimeMs
            )
            onActions(actions)
        } catch (e: Throwable) {
            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
            debugLog(DEBUG_ACTIONS, error = e) {
                "Error getting smart actions. Wait time: $waitTimeMs ms, actionType=$actionType"
            }
            val status =
                if (e is TimeoutException) {
                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
                } else {
                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR
                }
            notifyScreenshotOp(
                id,
                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
                status,
                waitTimeMs
            )
            onActions(listOf())
        }
    }

    private fun notifyScreenshotOp(
        screenshotId: String,
        op: ScreenshotNotificationSmartActionsProvider.ScreenshotOp,
        status: ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus,
        durationMs: Long
    ) {
        debugLog(DEBUG_ACTIONS) {
            "$smartActions notifyOp: $op id=$screenshotId, status=$status, durationMs=$durationMs"
        }
        try {
            smartActions.notifyOp(screenshotId, op, status, durationMs)
        } catch (e: Throwable) {
            Log.e(TAG, "Error in notifyScreenshotOp: ", e)
        }
    }

    private fun isSmartActionsEnabled(user: UserHandle): Boolean {
        // Smart actions don't yet work for cross-user saves.
        val savingToOtherUser = user !== Process.myUserHandle()
        val actionsEnabled =
            DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
                true
            )
        return !savingToOtherUser && actionsEnabled
    }

    companion object {
        private const val TAG = "SmartActionsProvider"
        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
    }
}
Loading