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

Commit 7f991aa6 authored by Miranda Kephart's avatar Miranda Kephart Committed by Android (Google) Code Review
Browse files

Merge "Implement screenshot smart actions with new action provider" into main

parents 5dcbe0bc 5fedbceb
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -34,9 +34,9 @@ import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.logging.UiEventLogger
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.res.R
import com.android.systemui.screenshot.scroll.ScrollCaptureController
import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
import com.android.systemui.screenshot.scroll.ScrollCaptureController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -113,7 +113,7 @@ constructor(
    override fun setChipIntents(imageData: ScreenshotController.SavedImageData) =
        view.setChipIntents(imageData)

    override fun requestDismissal(event: ScreenshotEvent) {
    override fun requestDismissal(event: ScreenshotEvent?) {
        if (DEBUG_DISMISS) {
            Log.d(TAG, "screenshot dismissal requested")
        }
@@ -124,7 +124,7 @@ constructor(
            }
            return
        }
        logger.log(event, 0, packageName)
        event?.let { logger.log(event, 0, packageName) }
        view.animateDismissal()
    }

+1 −0
Original line number Diff line number Diff line
@@ -165,6 +165,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
                    mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
                    mParams.owner);
            mImageData.subject = getSubjectString(mImageTime);
            mImageData.imageTime = mImageTime;

            mParams.mActionsReadyListener.onActionsReady(mImageData);
            if (DEBUG_CALLBACK) {
+107 −1
Original line number Diff line number Diff line
@@ -17,24 +17,37 @@
package com.android.systemui.screenshot

import android.app.ActivityOptions
import android.app.BroadcastOptions
import android.app.ExitTransitionCoordinator
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Process
import android.os.UserHandle
import android.provider.DeviceConfig
import android.util.Log
import android.util.Pair
import androidx.appcompat.content.res.AppCompatResources
import com.android.app.tracing.coroutines.launch
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.DebugLogger.debugLog
import com.android.systemui.res.R
import com.android.systemui.screenshot.ActionIntentCreator.createEdit
import com.android.systemui.screenshot.ActionIntentCreator.createShareWithSubject
import com.android.systemui.screenshot.ScreenshotController.SavedImageData
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.ActionButtonViewModel
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.text.DateFormat
import java.util.Date
import kotlinx.coroutines.CoroutineScope

/**
@@ -48,7 +61,9 @@ interface ScreenshotActionsProvider {
    interface Factory {
        fun create(
            request: ScreenshotData,
            requestId: String,
            windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
            requestDismissal: () -> Unit,
        ): ScreenshotActionsProvider
    }
}
@@ -59,9 +74,13 @@ constructor(
    private val context: Context,
    private val viewModel: ScreenshotViewModel,
    private val actionExecutor: ActionIntentExecutor,
    private val smartActionsProvider: SmartActionsProvider,
    private val uiEventLogger: UiEventLogger,
    @Application private val applicationScope: CoroutineScope,
    @Assisted val request: ScreenshotData,
    @Assisted val requestId: String,
    @Assisted val windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
    @Assisted val requestDismissal: () -> Unit,
) : ScreenshotActionsProvider {
    private var pendingAction: ((SavedImageData) -> Unit)? = null
    private var result: SavedImageData? = null
@@ -70,6 +89,7 @@ constructor(
    init {
        viewModel.setPreviewAction {
            debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" }
            uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString)
            onDeferrableActionTapped { result ->
                startSharedTransition(createEdit(result.uri, context), true)
            }
@@ -81,6 +101,7 @@ constructor(
                context.resources.getString(R.string.screenshot_edit_description),
            ) {
                debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
                uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
                onDeferrableActionTapped { result ->
                    startSharedTransition(createEdit(result.uri, context), true)
                }
@@ -93,11 +114,46 @@ constructor(
                context.resources.getString(R.string.screenshot_share_description),
            ) {
                debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" }
                uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString)
                onDeferrableActionTapped { result ->
                    startSharedTransition(createShareWithSubject(result.uri, result.subject), false)
                }
            }
        )
        if (smartActionsEnabled(request.userHandle ?: Process.myUserHandle())) {
            smartActionsProvider.requestQuickShare(request, requestId) { quickShare ->
                if (!quickShare.actionIntent.isImmutable) {
                    viewModel.addAction(
                        ActionButtonViewModel(
                            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
                                )
                                sendPendingIntent(
                                    smartActionsProvider
                                        .wrapIntent(
                                            quickShare,
                                            result.uri,
                                            result.subject,
                                            requestId
                                        )
                                        .actionIntent
                                )
                            }
                        }
                    )
                } else {
                    Log.w(TAG, "Received immutable quick share pending intent; ignoring")
                }
            }
        }
    }

    override fun setCompletedScreenshot(result: SavedImageData) {
@@ -105,12 +161,30 @@ constructor(
            Log.e(TAG, "Got a second completed screenshot for existing request!")
            return
        }
        if (result.uri == null || result.owner == null || result.subject == null) {
        if (result.uri == null || result.owner == null || result.imageTime == null) {
            Log.e(TAG, "Invalid result provided!")
            return
        }
        if (result.subject == null) {
            result.subject = getSubjectString(result.imageTime)
        }
        this.result = result
        pendingAction?.invoke(result)
        if (smartActionsEnabled(result.owner)) {
            smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions ->
                viewModel.addActions(
                    smartActions.map {
                        ActionButtonViewModel(
                            it.getIcon().loadDrawable(context),
                            it.title,
                            it.title
                        ) {
                            sendPendingIntent(it.actionIntent)
                        }
                    }
                )
            }
        }
    }

    override fun isPendingSharedTransition(): Boolean {
@@ -134,15 +208,47 @@ constructor(
        }
    }

    private fun sendPendingIntent(pendingIntent: PendingIntent) {
        try {
            val options = BroadcastOptions.makeBasic()
            options.setInteractive(true)
            options.setPendingIntentBackgroundActivityStartMode(
                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
            )
            pendingIntent.send(options.toBundle())
            requestDismissal.invoke()
        } catch (e: PendingIntent.CanceledException) {
            Log.e(TAG, "Intent cancelled", e)
        }
    }

    private fun smartActionsEnabled(user: UserHandle): Boolean {
        val savingToOtherUser = user != Process.myUserHandle()
        return !savingToOtherUser &&
            DeviceConfig.getBoolean(
                DeviceConfig.NAMESPACE_SYSTEMUI,
                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
                true
            )
    }

    private fun getSubjectString(imageTime: Long): String {
        val subjectDate = DateFormat.getDateTimeInstance().format(Date(imageTime))
        return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate)
    }

    @AssistedFactory
    interface Factory : ScreenshotActionsProvider.Factory {
        override fun create(
            request: ScreenshotData,
            requestId: String,
            windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
            requestDismissal: () -> Unit,
        ): DefaultScreenshotActionsProvider
    }

    companion object {
        private const val TAG = "ScreenshotActionsProvider"
        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
    }
}
+66 −20
Original line number Diff line number Diff line
@@ -96,7 +96,10 @@ import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;

import kotlin.Unit;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -167,6 +170,7 @@ public class ScreenshotController {
        public Notification.Action quickShareAction;
        public UserHandle owner;
        public String subject;  // Title for sharing
        public Long imageTime; // Time at which screenshot was saved

        /**
         * Used to reset the return data on error
@@ -176,6 +180,7 @@ public class ScreenshotController {
            smartActions = null;
            quickShareAction = null;
            subject = null;
            imageTime = null;
        }
    }

@@ -261,11 +266,9 @@ public class ScreenshotController {
    private SaveImageInBackgroundTask mSaveInBgTask;
    private boolean mScreenshotTakenInPortrait;
    private boolean mBlockAttach;

    private ScreenshotActionsProvider mActionsProvider;

    private Animator mScreenshotAnimation;
    private RequestCallback mCurrentRequestCallback;
    private ScreenshotActionsProvider mActionsProvider;
    private String mPackageName = "";
    private final BroadcastReceiver mCopyBroadcastReceiver;

@@ -317,6 +320,7 @@ public class ScreenshotController {
            @Assisted boolean showUIOnExternalDisplay
    ) {
        mScreenshotSmartActions = screenshotSmartActions;
        mActionsProviderFactory = actionsProviderFactory;
        mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
        mScrollCaptureClient = scrollCaptureClient;
        mUiEventLogger = uiEventLogger;
@@ -347,7 +351,6 @@ public class ScreenshotController {
        mAssistContentRequester = assistContentRequester;

        mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId);
        mActionsProviderFactory = actionsProviderFactory;

        mScreenshotHandler.setOnTimeoutRunnable(() -> {
            if (DEBUG_UI) {
@@ -441,8 +444,19 @@ public class ScreenshotController {
            return;
        }

        if (screenshotShelfUi()) {
            final UUID requestId = UUID.randomUUID();
            final String screenshotId = String.format("Screenshot_%s", requestId);
            mActionsProvider = mActionsProviderFactory.create(screenshot, screenshotId,
                    this::createWindowTransition, () -> {
                        mViewProxy.requestDismissal(null);
                        return Unit.INSTANCE;
                    });
            saveScreenshotInBackground(screenshot, requestId, finisher);
        } else {
            saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
                    this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
        }

        // The window is focusable by default
        setWindowFocusable(true);
@@ -477,8 +491,10 @@ public class ScreenshotController {
        // ignore system bar insets for the purpose of window layout
        mWindow.getDecorView().setOnApplyWindowInsetsListener(
                (v, insets) -> WindowInsets.CONSUMED);
        if (!screenshotShelfUi()) {
            mScreenshotHandler.cancelTimeout(); // restarted after animation
        }
    }

    private boolean shouldShowUi() {
        return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
@@ -497,11 +513,6 @@ public class ScreenshotController {

        mViewProxy.reset();

        if (screenshotShelfUi()) {
            mActionsProvider =
                    mActionsProviderFactory.create(screenshot, this::createWindowTransition);
        }

        if (mViewProxy.isAttachedToWindow()) {
            // if we didn't already dismiss for another reason
            if (!mViewProxy.isDismissing()) {
@@ -921,6 +932,39 @@ public class ScreenshotController {
        mScreenshotHandler.cancelTimeout();
    }

    private void saveScreenshotInBackground(
            ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
        ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
                requestId, screenshot.getBitmap(), screenshot.getUserHandle(), mDisplayId);
        future.addListener(() -> {
            try {
                ImageExporter.Result result = future.get();
                Log.d(TAG, "Saved screenshot: " + result);
                logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
                mScreenshotHandler.resetTimeout();
                if (result.uri != null) {
                    final SavedImageData savedImageData = new SavedImageData();
                    savedImageData.uri = result.uri;
                    savedImageData.owner = screenshot.getUserHandle();
                    savedImageData.imageTime = result.timestamp;
                    mActionsProvider.setCompletedScreenshot(savedImageData);
                    mViewProxy.setChipIntents(savedImageData);
                }
                if (DEBUG_CALLBACK) {
                    Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
                            + "finisher.accept(\"" + result.uri + "\"");
                }
                finisher.accept(result.uri);
            } catch (Exception e) {
                Log.d(TAG, "Failed to store screenshot", e);
                if (DEBUG_CALLBACK) {
                    Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
                }
                finisher.accept(null);
            }
        }, mMainExecutor);
    }

    /**
     * Creates a new worker thread and saves the screenshot to the media store.
     */
@@ -958,11 +1002,6 @@ public class ScreenshotController {
        logSuccessOnActionsReady(imageData);
        mScreenshotHandler.resetTimeout();

        if (screenshotShelfUi()) {
            mActionsProvider.setCompletedScreenshot(imageData);
            return;
        }

        if (imageData.uri != null) {
            if (DEBUG_UI) {
                Log.d(TAG, "Showing UI actions");
@@ -1014,20 +1053,27 @@ public class ScreenshotController {
    /**
     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
     */
    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
        if (imageData.uri == null) {
    private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
        if (uri == null) {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
            mNotificationsController.notifyScreenshotError(
                    R.string.screenshot_failed_to_save_text);
        } else {
            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
            if (mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
            if (mUserManager.isManagedProfile(owner.getIdentifier())) {
                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0,
                        mPackageName);
            }
        }
    }

    /**
     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
     */
    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
        logScreenshotResultStatus(imageData.uri, imageData.owner);
    }

    private boolean isUserSetupComplete(UserHandle owner) {
        return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+2 −2
Original line number Diff line number Diff line
@@ -98,7 +98,7 @@ constructor(

    override fun setChipIntents(imageData: SavedImageData) {}

    override fun requestDismissal(event: ScreenshotEvent) {
    override fun requestDismissal(event: ScreenshotEvent?) {
        debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }

        // If we're already animating out, don't restart the animation
@@ -106,7 +106,7 @@ constructor(
            debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
            return
        }
        logger.log(event, 0, packageName)
        event?.let { logger.log(it, 0, packageName) }
        val animator = animationController.getExitAnimation()
        animator.addListener(
            object : AnimatorListenerAdapter() {
Loading