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

Commit 5fedbceb authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Implement screenshot smart actions with new action provider

Bug: 329659738
Test: atest DefaultScreenshotActionsProviderTest
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT

Change-Id: I22e79d4d1658ac5b88d842220f89669a0246dab5
parent cea1ff0e
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