diff options
6 files changed, 275 insertions, 107 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index 97acccde2524..5019a6fcaf8b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -16,78 +16,133 @@ package com.android.systemui.screenshot +import android.app.ActivityOptions +import android.app.ExitTransitionCoordinator import android.content.Context import android.content.Intent -import android.graphics.drawable.Drawable -import android.net.Uri -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.systemui.dagger.qualifiers.Application +import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.res.R -import javax.inject.Inject +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.ui.viewmodel.ActionButtonViewModel +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 * implementation. */ interface ScreenshotActionsProvider { - data class ScreenshotAction( - val icon: Drawable? = null, - val text: String? = null, - val description: String, - val overrideTransition: Boolean = false, - val retrieveIntent: (Uri) -> Intent - ) - - interface ScreenshotActionsCallback { - fun setPreviewAction(overrideTransition: Boolean = false, retrieveIntent: (Uri) -> Intent) - fun addAction(action: ScreenshotAction) = addActions(listOf(action)) - fun addActions(actions: List<ScreenshotAction>) - } + fun setCompletedScreenshot(result: SavedImageData) + fun isPendingSharedTransition(): Boolean interface Factory { fun create( - context: Context, - user: UserHandle?, - callback: ScreenshotActionsCallback + request: ScreenshotData, + windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, ): ScreenshotActionsProvider } } -class DefaultScreenshotActionsProvider( +class DefaultScreenshotActionsProvider +@AssistedInject +constructor( private val context: Context, - private val user: UserHandle?, - private val callback: ScreenshotActionsProvider.ScreenshotActionsCallback + private val viewModel: ScreenshotViewModel, + private val actionExecutor: ActionIntentExecutor, + @Application private val applicationScope: CoroutineScope, + @Assisted val request: ScreenshotData, + @Assisted val windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, ) : ScreenshotActionsProvider { + private var pendingAction: ((SavedImageData) -> Unit)? = null + private var result: SavedImageData? = null + private var isPendingSharedTransition = false + init { - callback.setPreviewAction(true) { ActionIntentCreator.createEdit(it, context) } - val editAction = - ScreenshotActionsProvider.ScreenshotAction( + viewModel.setPreviewAction { + debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" } + onDeferrableActionTapped { result -> + startSharedTransition(createEdit(result.uri, context), true) + } + } + viewModel.addAction( + ActionButtonViewModel( AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), context.resources.getString(R.string.screenshot_edit_label), context.resources.getString(R.string.screenshot_edit_description), - true - ) { uri -> - ActionIntentCreator.createEdit(uri, context) + ) { + debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" } + onDeferrableActionTapped { result -> + startSharedTransition(createEdit(result.uri, context), true) + } } - val shareAction = - ScreenshotActionsProvider.ScreenshotAction( + ) + viewModel.addAction( + ActionButtonViewModel( AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), context.resources.getString(R.string.screenshot_share_label), context.resources.getString(R.string.screenshot_share_description), - false - ) { uri -> - ActionIntentCreator.createShare(uri) + ) { + debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" } + onDeferrableActionTapped { result -> + startSharedTransition(createShareWithSubject(result.uri, result.subject), false) + } } - callback.addActions(listOf(editAction, shareAction)) + ) } - class Factory @Inject constructor() : ScreenshotActionsProvider.Factory { - override fun create( - context: Context, - user: UserHandle?, - callback: ScreenshotActionsProvider.ScreenshotActionsCallback - ): ScreenshotActionsProvider { - return DefaultScreenshotActionsProvider(context, user, callback) + override fun setCompletedScreenshot(result: SavedImageData) { + if (this.result != null) { + Log.e(TAG, "Got a second completed screenshot for existing request!") + return + } + if (result.uri == null || result.owner == null || result.subject == null) { + Log.e(TAG, "Invalid result provided!") + return } + this.result = result + pendingAction?.invoke(result) + } + + override fun isPendingSharedTransition(): Boolean { + return isPendingSharedTransition + } + + private fun onDeferrableActionTapped(onResult: (SavedImageData) -> Unit) { + result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult } + } + + private fun startSharedTransition(intent: Intent, overrideTransition: Boolean) { + val user = + result?.owner + ?: run { + Log.wtf(TAG, "User handle not provided in screenshot result! Result: $result") + return + } + isPendingSharedTransition = true + applicationScope.launch("$TAG#launchIntentAsync") { + actionExecutor.launchIntent(intent, windowTransition.invoke(), user, overrideTransition) + } + } + + @AssistedFactory + interface Factory : ScreenshotActionsProvider.Factory { + override fun create( + request: ScreenshotData, + windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, + ): DefaultScreenshotActionsProvider + } + + companion object { + private const val TAG = "ScreenshotActionsProvider" } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 047ecb42287b..b43137fa2a74 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -498,8 +498,8 @@ public class ScreenshotController { mViewProxy.reset(); if (screenshotShelfUi()) { - mActionsProvider = mActionsProviderFactory.create(mContext, screenshot.getUserHandle(), - ((ScreenshotActionsProvider.ScreenshotActionsCallback) mViewProxy)); + mActionsProvider = + mActionsProviderFactory.create(screenshot, this::createWindowTransition); } if (mViewProxy.isAttachedToWindow()) { @@ -529,7 +529,11 @@ public class ScreenshotController { } boolean isPendingSharedTransition() { - return mViewProxy.isPendingSharedTransition(); + if (screenshotShelfUi()) { + return mActionsProvider != null && mActionsProvider.isPendingSharedTransition(); + } else { + return mViewProxy.isPendingSharedTransition(); + } } // Any cleanup needed when the service is being destroyed. @@ -682,7 +686,8 @@ public class ScreenshotController { mImageCapture.captureDisplay(mDisplayId, getFullScreenRect()); if (newScreenshot != null) { - // delay starting scroll capture to make sure scrim is up before the app moves + // delay starting scroll capture to make sure scrim is up before the app + // moves mViewProxy.prepareScrollingTransition( response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait, () -> runBatchScrollCapture(response, owner)); @@ -951,13 +956,17 @@ public class ScreenshotController { */ private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { logSuccessOnActionsReady(imageData); - if (DEBUG_UI) { - Log.d(TAG, "Showing UI actions"); - } - mScreenshotHandler.resetTimeout(); + if (screenshotShelfUi()) { + mActionsProvider.setCompletedScreenshot(imageData); + return; + } + if (imageData.uri != null) { + if (DEBUG_UI) { + Log.d(TAG, "Showing UI actions"); + } if (!imageData.owner.equals(Process.myUserHandle())) { Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as " + imageData.uri); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt index 88bca951beb6..defddc30586e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -20,10 +20,8 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.Notification import android.content.Context -import android.content.Intent import android.graphics.Bitmap import android.graphics.Rect -import android.net.Uri import android.view.KeyEvent import android.view.LayoutInflater import android.view.ScrollCaptureResponse @@ -35,7 +33,6 @@ import android.window.OnBackInvokedDispatcher import com.android.internal.logging.UiEventLogger import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.res.R -import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW @@ -45,7 +42,6 @@ import com.android.systemui.screenshot.scroll.ScrollCaptureController import com.android.systemui.screenshot.ui.ScreenshotAnimationController import com.android.systemui.screenshot.ui.ScreenshotShelfView import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder -import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -59,7 +55,7 @@ constructor( private val viewModel: ScreenshotViewModel, @Assisted private val context: Context, @Assisted private val displayId: Int -) : ScreenshotViewProxy, ScreenshotActionsProvider.ScreenshotActionsCallback { +) : ScreenshotViewProxy { override val view: ScreenshotShelfView = LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView override val screenshotPreview: View @@ -77,8 +73,6 @@ constructor( override var isPendingSharedTransition = false private val animationController = ScreenshotAnimationController(view) - private var imageData: SavedImageData? = null - private var runOnImageDataAcquired: ((SavedImageData) -> Unit)? = null init { ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) @@ -91,9 +85,7 @@ constructor( override fun reset() { animationController.cancel() isPendingSharedTransition = false - imageData = null viewModel.reset() - runOnImageDataAcquired = null } override fun updateInsets(insets: WindowInsets) {} override fun updateOrientation(insets: WindowInsets) {} @@ -104,10 +96,7 @@ constructor( override fun addQuickShareChip(quickShareAction: Notification.Action) {} - override fun setChipIntents(data: SavedImageData) { - imageData = data - runOnImageDataAcquired?.invoke(data) - } + override fun setChipIntents(imageData: SavedImageData) {} override fun requestDismissal(event: ScreenshotEvent) { debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } @@ -143,13 +132,18 @@ constructor( newScreenshot: Bitmap, screenshotTakenInPortrait: Boolean, onTransitionPrepared: Runnable, - ) {} + ) { + onTransitionPrepared.run() + } override fun startLongScreenshotTransition( transitionDestination: Rect, onTransitionEnd: Runnable, longScreenshot: ScrollCaptureController.LongScreenshot - ) {} + ) { + onTransitionEnd.run() + callbacks?.onDismiss() + } override fun restoreNonScrollingUi() {} @@ -219,41 +213,4 @@ constructor( interface Factory : ScreenshotViewProxy.Factory { override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy } - - override fun setPreviewAction(overrideTransition: Boolean, retrieveIntent: (Uri) -> Intent) { - viewModel.setPreviewAction { - imageData?.let { - val intent = retrieveIntent(it.uri) - debugLog(DEBUG_ACTIONS) { "Preview tapped: $intent" } - isPendingSharedTransition = true - callbacks?.onAction(intent, it.owner, overrideTransition) - } - } - } - - override fun addActions(actions: List<ScreenshotActionsProvider.ScreenshotAction>) { - viewModel.addActions( - actions.map { action -> - ActionButtonViewModel(action.icon, action.text, action.description) { - val actionRunnable = - getActionRunnable(action.retrieveIntent, action.overrideTransition) - imageData?.let { actionRunnable(it) } - ?: run { runOnImageDataAcquired = actionRunnable } - } - } - ) - } - - private fun getActionRunnable( - retrieveIntent: (Uri) -> Intent, - overrideTransition: Boolean - ): (SavedImageData) -> Unit { - val onClick: (SavedImageData) -> Unit = { - val intent = retrieveIntent(it.uri) - debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } - isPendingSharedTransition = true - callbacks!!.onAction(intent, it.owner, overrideTransition) - } - return onClick - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt index d8782009e24b..ea05884096c8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -60,7 +60,7 @@ object ScreenshotShelfViewBinder { } launch { viewModel.previewAction.collect { onClick -> - previewView.setOnClickListener { onClick?.run() } + previewView.setOnClickListener { onClick?.invoke() } } } launch { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt index dc61d1e9c37b..ddfa69b687eb 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.StateFlow class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { private val _preview = MutableStateFlow<Bitmap?>(null) val preview: StateFlow<Bitmap?> = _preview - private val _previewAction = MutableStateFlow<Runnable?>(null) - val previewAction: StateFlow<Runnable?> = _previewAction + private val _previewAction = MutableStateFlow<(() -> Unit)?>(null) + val previewAction: StateFlow<(() -> Unit)?> = _previewAction private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) val actions: StateFlow<List<ActionButtonViewModel>> = _actions val showDismissButton: Boolean @@ -35,8 +35,14 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager _preview.value = bitmap } - fun setPreviewAction(runnable: Runnable) { - _previewAction.value = runnable + fun setPreviewAction(onClick: () -> Unit) { + _previewAction.value = onClick + } + + fun addAction(action: ActionButtonViewModel) { + val actionList = _actions.value.toMutableList() + actionList.add(action) + _actions.value = actionList } fun addActions(actions: List<ActionButtonViewModel>) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt new file mode 100644 index 000000000000..f49c6e25c2d3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt @@ -0,0 +1,141 @@ +/* + * 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.ExitTransitionCoordinator +import android.content.Intent +import android.net.Uri +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.view.accessibility.AccessibilityManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Ignore +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.Assert.assertNotNull +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.kotlin.verifyBlocking + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class DefaultScreenshotActionsProviderTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = StandardTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) + + private val actionIntentExecutor = mock<ActionIntentExecutor>() + private val accessibilityManager = mock<AccessibilityManager>() + private val transition = mock<android.util.Pair<ActivityOptions, ExitTransitionCoordinator>>() + + private val request = ScreenshotData.forTesting() + private val invalidResult = ScreenshotController.SavedImageData() + private val validResult = + ScreenshotController.SavedImageData().apply { + uri = Uri.EMPTY + owner = UserHandle.CURRENT + subject = "Test" + } + + private lateinit var viewModel: ScreenshotViewModel + private lateinit var actionsProvider: ScreenshotActionsProvider + + @Before + fun setUp() { + viewModel = ScreenshotViewModel(accessibilityManager) + actionsProvider = + DefaultScreenshotActionsProvider( + context, + viewModel, + actionIntentExecutor, + testScope, + request + ) { + transition + } + } + + @Test + fun previewActionAccessed_beforeScreenshotCompleted_doesNothing() { + assertNotNull(viewModel.previewAction.value) + viewModel.previewAction.value!!.invoke() + verifyNoMoreInteractions(actionIntentExecutor) + } + + @Test + fun actionButtonsAccessed_beforeScreenshotCompleted_doesNothing() { + assertThat(viewModel.actions.value.size).isEqualTo(2) + val firstAction = viewModel.actions.value[0] + assertThat(firstAction.onClicked).isNotNull() + val secondAction = viewModel.actions.value[1] + assertThat(secondAction.onClicked).isNotNull() + firstAction.onClicked!!.invoke() + secondAction.onClicked!!.invoke() + verifyNoMoreInteractions(actionIntentExecutor) + } + + @Test + fun actionAccessed_withInvalidResult_doesNothing() { + actionsProvider.setCompletedScreenshot(invalidResult) + viewModel.previewAction.value!!.invoke() + viewModel.actions.value[1].onClicked!!.invoke() + + verifyNoMoreInteractions(actionIntentExecutor) + } + + @Test + @Ignore("b/332526567") + fun actionAccessed_withResult_launchesIntent() = runTest { + actionsProvider.setCompletedScreenshot(validResult) + viewModel.actions.value[0].onClicked!!.invoke() + scheduler.advanceUntilIdle() + + val intentCaptor = argumentCaptor<Intent>() + verifyBlocking(actionIntentExecutor) { + launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(true)) + } + assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT) + } + + @Test + @Ignore("b/332526567") + fun actionAccessed_whilePending_launchesMostRecentAction() = runTest { + viewModel.actions.value[0].onClicked!!.invoke() + viewModel.previewAction.value!!.invoke() + viewModel.actions.value[1].onClicked!!.invoke() + actionsProvider.setCompletedScreenshot(validResult) + scheduler.advanceUntilIdle() + + val intentCaptor = argumentCaptor<Intent>() + verifyBlocking(actionIntentExecutor) { + launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(false)) + } + assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CHOOSER) + } +} |