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

Commit f80effdd authored by Miranda Kephart's avatar Miranda Kephart Committed by Matt Casey
Browse files

Abstract screenshot actions into ActionExecutor

The actions need to do things involving the window/dismissal. It
doesn't make sense to give the action provider direct access to these
references; instead pull it into an ActionExecutor that can e.g. start
shared transitions and send pending intents.

Bug: 329659738
Test: atest DefaultScreenshotActionsProviderTest
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT
Change-Id: I7f5a9fa95a9106c76fcbacdb94db4a7568f56490
Merged-In: I7f5a9fa95a9106c76fcbacdb94db4a7568f56490
parent 431c5649
Loading
Loading
Loading
Loading
+106 −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.app.ActivityOptions
import android.app.BroadcastOptions
import android.app.ExitTransitionCoordinator
import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks
import android.app.PendingIntent
import android.content.Intent
import android.os.UserHandle
import android.util.Log
import android.util.Pair
import android.view.View
import android.view.Window
import com.android.app.tracing.coroutines.launch
import com.android.internal.app.ChooserActivity
import com.android.systemui.dagger.qualifiers.Application
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope

class ActionExecutor
@AssistedInject
constructor(
    private val intentExecutor: ActionIntentExecutor,
    @Application private val applicationScope: CoroutineScope,
    @Assisted val window: Window,
    @Assisted val transitionView: View,
    @Assisted val onDismiss: (() -> Unit)
) {

    var isPendingSharedTransition = false
        private set

    fun startSharedTransition(intent: Intent, user: UserHandle, overrideTransition: Boolean) {
        isPendingSharedTransition = true
        applicationScope.launch("$TAG#launchIntentAsync") {
            intentExecutor.launchIntent(intent, createWindowTransition(), user, overrideTransition)
        }
    }

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

    /**
     * Supplies the necessary bits for the shared element transition to share sheet. Note that once
     * called, the action intent to share must be sent immediately after.
     */
    private fun createWindowTransition(): Pair<ActivityOptions, ExitTransitionCoordinator> {
        val callbacks: ExitTransitionCallbacks =
            object : ExitTransitionCallbacks {
                override fun isReturnTransitionAllowed(): Boolean {
                    return false
                }

                override fun hideSharedElements() {
                    isPendingSharedTransition = false
                    onDismiss.invoke()
                }

                override fun onFinish() {}
            }
        return ActivityOptions.startSharedElementAnimation(
            window,
            callbacks,
            null,
            Pair.create(transitionView, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
        )
    }

    @AssistedFactory
    interface Factory {
        fun create(window: Window, transitionView: View, onDismiss: (() -> Unit)): ActionExecutor
    }

    companion object {
        private const val TAG = "ActionExecutor"
    }
}
+23 −58
Original line number Diff line number Diff line
@@ -16,20 +16,11 @@

package com.android.systemui.screenshot

import android.app.ActivityOptions
import android.app.BroadcastOptions
import android.app.ExitTransitionCoordinator
import android.app.PendingIntent
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.os.UserHandle
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.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
@@ -43,7 +34,6 @@ import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope

/**
 * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI
@@ -51,7 +41,6 @@ import kotlinx.coroutines.CoroutineScope
 */
interface ScreenshotActionsProvider {
    fun setCompletedScreenshot(result: ScreenshotSavedResult)
    fun isPendingSharedTransition(): Boolean

    fun onAssistContentAvailable(assistContent: AssistContent) {}

@@ -59,8 +48,7 @@ interface ScreenshotActionsProvider {
        fun create(
            request: ScreenshotData,
            requestId: String,
            windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
            requestDismissal: () -> Unit,
            actionExecutor: ActionExecutor,
        ): ScreenshotActionsProvider
    }
}
@@ -70,25 +58,25 @@ class DefaultScreenshotActionsProvider
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,
    @Assisted val actionExecutor: ActionExecutor,
) : ScreenshotActionsProvider {
    private var pendingAction: ((ScreenshotSavedResult) -> Unit)? = null
    private var result: ScreenshotSavedResult? = null
    private var isPendingSharedTransition = false

    init {
        viewModel.setPreviewAction {
            debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" }
            uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString)
            onDeferrableActionTapped { result ->
                startSharedTransition(createEdit(result.uri, context), result.user, true)
                actionExecutor.startSharedTransition(
                    createEdit(result.uri, context),
                    result.user,
                    true
                )
            }
        }
        viewModel.addAction(
@@ -100,7 +88,11 @@ constructor(
                debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
                uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
                onDeferrableActionTapped { result ->
                    startSharedTransition(createEdit(result.uri, context), result.user, true)
                    actionExecutor.startSharedTransition(
                        createEdit(result.uri, context),
                        result.user,
                        true
                    )
                }
            }
        )
@@ -113,7 +105,7 @@ constructor(
                debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" }
                uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString)
                onDeferrableActionTapped { result ->
                    startSharedTransition(
                    actionExecutor.startSharedTransition(
                        createShareWithSubject(result.uri, result.subject),
                        result.user,
                        false
@@ -136,11 +128,14 @@ constructor(
                                0,
                                request.packageNameString
                            )
                            sendPendingIntent(
                                smartActionsProvider
                                    .wrapIntent(quickShare, result.uri, result.subject, requestId)
                                    .actionIntent
                            val pendingIntentWithUri =
                                smartActionsProvider.wrapIntent(
                                    quickShare,
                                    result.uri,
                                    result.subject,
                                    requestId
                                )
                            actionExecutor.sendPendingIntent(pendingIntentWithUri)
                        }
                    }
                )
@@ -161,53 +156,23 @@ constructor(
            viewModel.addActions(
                smartActions.map {
                    ActionButtonViewModel(it.getIcon().loadDrawable(context), it.title, it.title) {
                        sendPendingIntent(it.actionIntent)
                        actionExecutor.sendPendingIntent(it.actionIntent)
                    }
                }
            )
        }
    }

    override fun isPendingSharedTransition(): Boolean {
        return isPendingSharedTransition
    }

    private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) {
        result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult }
    }

    private fun startSharedTransition(
        intent: Intent,
        user: UserHandle,
        overrideTransition: Boolean
    ) {
        isPendingSharedTransition = true
        applicationScope.launch("$TAG#launchIntentAsync") {
            actionExecutor.launchIntent(intent, windowTransition.invoke(), user, overrideTransition)
        }
    }

    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)
        }
    }

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

+15 −10
Original line number Diff line number Diff line
@@ -242,6 +242,7 @@ public class ScreenshotController {
    private final ExecutorService mBgExecutor;
    private final BroadcastSender mBroadcastSender;
    private final BroadcastDispatcher mBroadcastDispatcher;
    private final ActionExecutor mActionExecutor;

    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mWindowLayoutParams;
@@ -257,7 +258,7 @@ public class ScreenshotController {
    private final ScreenshotNotificationSmartActionsProvider
            mScreenshotNotificationSmartActionsProvider;
    private final TimeoutHandler mScreenshotHandler;
    private final ActionIntentExecutor mActionExecutor;
    private final ActionIntentExecutor mActionIntentExecutor;
    private final UserManager mUserManager;
    private final AssistContentRequester mAssistContentRequester;

@@ -311,7 +312,8 @@ public class ScreenshotController {
            BroadcastSender broadcastSender,
            BroadcastDispatcher broadcastDispatcher,
            ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
            ActionIntentExecutor actionExecutor,
            ActionIntentExecutor actionIntentExecutor,
            ActionExecutor.Factory actionExecutorFactory,
            UserManager userManager,
            AssistContentRequester assistContentRequester,
            MessageContainerController messageContainerController,
@@ -345,7 +347,7 @@ public class ScreenshotController {
        final Context displayContext = context.createDisplayContext(getDisplay());
        mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
        mFlags = flags;
        mActionExecutor = actionExecutor;
        mActionIntentExecutor = actionIntentExecutor;
        mUserManager = userManager;
        mMessageContainerController = messageContainerController;
        mAssistContentRequester = assistContentRequester;
@@ -369,6 +371,12 @@ public class ScreenshotController {
        mConfigChanges.applyNewConfig(context.getResources());
        reloadAssets();

        mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy.getScreenshotPreview(),
                () -> {
                    requestDismissal(null);
                    return Unit.INSTANCE;
                });

        // Sound is only reproduced from the controller of the default display.
        if (displayId == Display.DEFAULT_DISPLAY) {
            mScreenshotSoundController = screenshotSoundController.get();
@@ -447,11 +455,8 @@ public class ScreenshotController {
        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;
                    });
            mActionsProvider = mActionsProviderFactory.create(
                    screenshot, screenshotId, mActionExecutor);
            saveScreenshotInBackground(screenshot, requestId, finisher);

            if (screenshot.getTaskId() >= 0) {
@@ -548,7 +553,7 @@ public class ScreenshotController {

    boolean isPendingSharedTransition() {
        if (screenshotShelfUi()) {
            return mActionsProvider != null && mActionsProvider.isPendingSharedTransition();
            return mActionExecutor.isPendingSharedTransition();
        } else {
            return mViewProxy.isPendingSharedTransition();
        }
@@ -599,7 +604,7 @@ public class ScreenshotController {

            @Override
            public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) {
                mActionExecutor.launchIntentAsync(
                mActionIntentExecutor.launchIntentAsync(
                        intent, createWindowTransition(), owner, overrideTransition);
            }

+8 −13
Original line number Diff line number Diff line
@@ -106,14 +106,14 @@ constructor(
     * @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 wrapped action
     * @return the pending intent with correct URI
     */
    fun wrapIntent(
        quickShare: Notification.Action,
        uri: Uri,
        subject: String,
        id: String
    ): Notification.Action {
    ): PendingIntent {
        val wrappedIntent: Intent =
            Intent(context, SmartActionsReceiver::class.java)
                .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
@@ -134,17 +134,12 @@ constructor(
            .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType)
            .putExtra(ScreenshotController.EXTRA_ID, id)
            .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, true)
        val broadcastIntent =
            PendingIntent.getBroadcast(
        return PendingIntent.getBroadcast(
            context,
            Random.nextInt(),
            wrappedIntent,
            PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        return Notification.Action.Builder(quickShare.getIcon(), quickShare.title, broadcastIntent)
            .setContextual(true)
            .addExtras(extras)
            .build()
    }

    private fun createFillInIntent(uri: Uri, subject: String): Intent {
+85 −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.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.view.View
import android.view.Window
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking

@RunWith(AndroidTestingRunner::class)
@SmallTest
class ActionExecutorTest : SysuiTestCase() {
    private val scheduler = TestCoroutineScheduler()
    private val mainDispatcher = StandardTestDispatcher(scheduler)
    private val testScope = TestScope(mainDispatcher)

    private val intentExecutor = mock<ActionIntentExecutor>()
    private val window = mock<Window>()
    private val view = mock<View>()
    private val onDismiss = mock<(() -> Unit)>()
    private val pendingIntent = mock<PendingIntent>()

    private lateinit var actionExecutor: ActionExecutor

    @Test
    fun startSharedTransition_callsLaunchIntent() = runTest {
        actionExecutor = createActionExecutor()

        actionExecutor.startSharedTransition(Intent(Intent.ACTION_EDIT), UserHandle.CURRENT, true)
        scheduler.advanceUntilIdle()

        val intentCaptor = argumentCaptor<Intent>()
        verifyBlocking(intentExecutor) {
            launchIntent(capture(intentCaptor), any(), eq(UserHandle.CURRENT), eq(true))
        }
        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT)
    }

    @Test
    fun sendPendingIntent_dismisses() = runTest {
        actionExecutor = createActionExecutor()

        actionExecutor.sendPendingIntent(pendingIntent)

        verify(pendingIntent).send(any(Bundle::class.java))
        verify(onDismiss).invoke()
    }

    private fun createActionExecutor(): ActionExecutor {
        return ActionExecutor(intentExecutor, testScope, window, view, onDismiss)
    }
}
Loading