diff options
5 files changed, 101 insertions, 44 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt index 0448ad517c6d..0a82f72f8f21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.shared.Flags import com.google.common.truth.Truth.assertThat import java.util.UUID import kotlin.test.Test +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.runner.RunWith import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.kotlin.any @@ -43,10 +46,19 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class DefaultScreenshotActionsProviderTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = UnconfinedTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) private val actionExecutor = mock<ActionExecutor>() private val uiEventLogger = mock<UiEventLogger>() private val actionsCallback = mock<ScreenshotActionsController.ActionsCallback>() - private val actionIntentCreator = ActionIntentCreator(context, context.packageManager) + private val actionIntentCreator = + ActionIntentCreator( + context, + context.packageManager, + testScope.backgroundScope, + mainDispatcher, + ) private val request = ScreenshotData.forTesting(userHandle = UserHandle.OWNER) private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0) @@ -198,6 +210,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { context, uiEventLogger, actionIntentCreator, + testScope, UUID.randomUUID(), request, actionExecutor, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 9208fc3dd016..271f4dce0aab 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -23,18 +23,31 @@ import android.content.ContentProvider import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException import android.net.Uri import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.screenshot.scroll.LongScreenshotActivity import com.android.systemui.shared.Flags.usePreferredImageEditor +import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SysUISingleton class ActionIntentCreator @Inject -constructor(private val context: Context, private val packageManager: PackageManager) { +constructor( + private val context: Context, + private val packageManager: PackageManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { /** @return a chooser intent to share the given URI. */ fun createShare(uri: Uri): Intent = createShare(uri, subject = null, text = null) @@ -76,11 +89,16 @@ constructor(private val context: Context, private val packageManager: PackageMan .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } + // Non-suspend version for java compat + fun createEdit(rawUri: Uri, consumer: Consumer<Intent>) { + applicationScope.launch { consumer.accept(createEdit(rawUri)) } + } + /** * @return an ACTION_EDIT intent for the given URI, directed to config_preferredScreenshotEditor * if enabled, falling back to config_screenshotEditor if that's non-empty. */ - fun createEdit(rawUri: Uri): Intent { + suspend fun createEdit(rawUri: Uri): Intent { val uri = uriWithoutUserId(rawUri) val editIntent = Intent(Intent.ACTION_EDIT) @@ -112,22 +130,30 @@ constructor(private val context: Context, private val packageManager: PackageMan .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) } - private fun preferredEditor(): ComponentName? = + private suspend fun preferredEditor(): ComponentName? = runCatching { val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor) val component = ComponentName.unflattenFromString(preferredEditor) ?: return null + return if (isComponentAvailable(component)) component else null + } + .getOrNull() + + private suspend fun isComponentAvailable(component: ComponentName): Boolean = + withContext(backgroundDispatcher) { + try { val info = packageManager.getPackageInfo( component.packageName, PackageManager.GET_ACTIVITIES, ) - - return info.activities - ?.firstOrNull { it.componentName.className.equals(component.className) } - ?.componentName + info.activities?.firstOrNull { + it.componentName.className == component.className + } != null + } catch (e: NameNotFoundException) { + false } - .getOrNull() + } private fun defaultEditor(): ComponentName? = runCatching { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index 4373389f4a51..d91f267ff3ca 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -22,6 +22,7 @@ import android.net.Uri import android.util.Log import androidx.appcompat.content.res.AppCompatResources 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.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED @@ -34,6 +35,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI @@ -68,6 +71,7 @@ constructor( private val context: Context, private val uiEventLogger: UiEventLogger, private val actionIntentCreator: ActionIntentCreator, + @Application private val applicationScope: CoroutineScope, @Assisted val requestId: UUID, @Assisted val request: ScreenshotData, @Assisted val actionExecutor: ActionExecutor, @@ -75,7 +79,7 @@ constructor( ) : ScreenshotActionsProvider { private var addedScrollChip = false private var onScrollClick: Runnable? = null - private var pendingAction: ((ScreenshotSavedResult) -> Unit)? = null + private var pendingAction: (suspend (ScreenshotSavedResult) -> Unit)? = null private var result: ScreenshotSavedResult? = null private var webUri: Uri? = null @@ -166,15 +170,16 @@ constructor( return } this.result = result - pendingAction?.invoke(result) + pendingAction?.also { applicationScope.launch { it.invoke(result) } } } override fun onAssistContent(assistContent: AssistContent?) { webUri = assistContent?.webUri } - private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) { - result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult } + private fun onDeferrableActionTapped(onResult: suspend (ScreenshotSavedResult) -> Unit) { + result?.let { applicationScope.launch { onResult.invoke(it) } } + ?: run { pendingAction = onResult } } @AssistedFactory @@ -188,6 +193,6 @@ constructor( } companion object { - private const val TAG = "ScreenshotActionsProvider" + private const val TAG = "ScreenshotActionsPrvdr" } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java index ecea30f1b1c3..88ffd4fa2750 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -352,30 +352,35 @@ public class LongScreenshotActivity extends Activity { private void doEdit(Uri uri) { if (mScreenshotUserHandle != Process.myUserHandle()) { // TODO: Fix transition for work profile. Omitting it in the meantime. - mActionExecutor.launchIntentAsync( - mActionIntentCreator.createEdit(uri), - mScreenshotUserHandle, false, - /* activityOptions */ null, /* transitionCoordinator */ null); + mActionIntentCreator.createEdit(uri, intent -> { + mActionExecutor.launchIntentAsync( + intent, + mScreenshotUserHandle, false, + /* activityOptions */ null, /* transitionCoordinator */ null); + }); + } else { if (usePreferredImageEditor()) { - Intent intent = mActionIntentCreator.createEdit(uri); - Bundle options = null; - - if (intent.getComponent() != null) { - // Modify intent for shared transition if we're opening a specific editor. - intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.removeFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - mTransitionView.setImageBitmap(mOutputBitmap); - mTransitionView.setVisibility(View.VISIBLE); - mTransitionView.setTransitionName( - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); - options = ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView, - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle(); - // TODO: listen for transition completing instead of finishing onStop - mTransitionStarted = true; - } + mActionIntentCreator.createEdit(uri, intent -> { + Bundle options = null; + + if (intent.getComponent() != null) { + // Modify intent for shared transition if we're opening a specific editor. + intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.removeFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + mTransitionView.setImageBitmap(mOutputBitmap); + mTransitionView.setVisibility(View.VISIBLE); + mTransitionView.setTransitionName( + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); + options = ActivityOptions.makeSceneTransitionAnimation(this, + mTransitionView, + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle(); + // TODO: listen for transition completing instead of finishing onStop + mTransitionStarted = true; + } - startActivity(intent, options); + startActivity(intent, options); + }); } else { String editorPackage = getString(R.string.config_screenshotEditor); Intent intent = new Intent(Intent.ACTION_EDIT); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt index 600572545d55..1f189a540aa2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -35,6 +35,10 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -43,9 +47,13 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class ActionIntentCreatorTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = UnconfinedTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) val context = mock<Context>() val packageManager = mock<PackageManager>() - private val actionIntentCreator = ActionIntentCreator(context, packageManager) + private val actionIntentCreator = + ActionIntentCreator(context, packageManager, testScope.backgroundScope, mainDispatcher) @Test fun testCreateShare() { @@ -132,7 +140,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy() { + fun testCreateEditLegacy() = runTest { val uri = Uri.parse("content://fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -155,7 +163,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy_embeddedUserIdRemoved() { + fun testCreateEditLegacy_embeddedUserIdRemoved() = runTest { val uri = Uri.parse("content://555@fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -166,7 +174,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy_withEditor() { + fun testCreateEditLegacy_withEditor() = runTest { val uri = Uri.parse("content://fake") val component = ComponentName("com.android.foo", "com.android.foo.Something") @@ -180,7 +188,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit() { + fun testCreateEdit() = runTest { val uri = Uri.parse("content://fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -203,7 +211,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_embeddedUserIdRemoved() { + fun testCreateEdit_embeddedUserIdRemoved() = runTest { val uri = Uri.parse("content://555@fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -214,7 +222,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withPreferredEditorEnabled() { + fun testCreateEdit_withPreferredEditorEnabled() = runTest { val uri = Uri.parse("content://fake") val fallbackComponent = ComponentName("com.android.foo", "com.android.foo.Something") val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") @@ -243,7 +251,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withPreferredEditorDisabled() { + fun testCreateEdit_withPreferredEditorDisabled() = runTest { val uri = Uri.parse("content://fake") val fallbackComponent = ComponentName("com.android.foo", "com.android.foo.Something") val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") @@ -266,7 +274,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withFallbackEditor() { + fun testCreateEdit_withFallbackEditor() = runTest { val uri = Uri.parse("content://fake") val component = ComponentName("com.android.foo", "com.android.foo.Something") |