diff options
8 files changed, 204 insertions, 38 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 cd30eddb1901..0448ad517c6d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt @@ -46,6 +46,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { 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 request = ScreenshotData.forTesting(userHandle = UserHandle.OWNER) private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0) @@ -196,6 +197,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { return DefaultScreenshotActionsProvider( context, uiEventLogger, + actionIntentCreator, UUID.randomUUID(), request, actionExecutor, diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 8d10e393b5ca..b273886e286e 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -501,7 +501,16 @@ <!-- Smartspace trampoline activity that is used when the user taps smartspace. --> <string name="config_smartspaceTrampolineActivityComponent" translatable="false">com.google.android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity</string> - <!-- Screenshot editing default activity. Must handle ACTION_EDIT image/png intents. + <!-- Screenshot editing default activity. Will only be used if the provided component exists and + is enabled. + + Must handle ACTION_EDIT image/png intents. + Blank falls back to config_screenshotEditor. + This name is in the ComponentName flattened format (package/class) --> + <string name="config_preferredScreenshotEditor" translatable="false"></string> + + <!-- Screenshot editing activity used if config_preferredScreenshotEditor is not available. + Must handle ACTION_EDIT image/png intents. Blank sends the user to the Chooser first. This name is in the ComponentName flattened format (package/class) --> <string name="config_screenshotEditor" translatable="false"></string> diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 15638d3496e9..9208fc3dd016 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -22,12 +22,19 @@ import android.content.ComponentName import android.content.ContentProvider import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.os.UserHandle +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import com.android.systemui.screenshot.scroll.LongScreenshotActivity +import com.android.systemui.shared.Flags.usePreferredImageEditor +import javax.inject.Inject -object ActionIntentCreator { +@SysUISingleton +class ActionIntentCreator +@Inject +constructor(private val context: Context, private val packageManager: PackageManager) { /** @return a chooser intent to share the given URI. */ fun createShare(uri: Uri): Intent = createShare(uri, subject = null, text = null) @@ -54,7 +61,7 @@ object ActionIntentCreator { clipData = ClipData( ClipDescription("content", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)), - ClipData.Item(uri) + ClipData.Item(uri), ) subject?.let { putExtra(Intent.EXTRA_SUBJECT, subject) } @@ -70,16 +77,21 @@ object ActionIntentCreator { } /** - * @return an ACTION_EDIT intent for the given URI, directed to config_screenshotEditor if - * available. + * @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, context: Context): Intent { + fun createEdit(rawUri: Uri): Intent { val uri = uriWithoutUserId(rawUri) val editIntent = Intent(Intent.ACTION_EDIT) - val editor = context.getString(R.string.config_screenshotEditor) - if (editor.isNotEmpty()) { - editIntent.component = ComponentName.unflattenFromString(editor) + if (usePreferredImageEditor()) { + // Use the preferred editor if it's available, otherwise fall back to the default editor + editIntent.component = preferredEditor() ?: defaultEditor() + } else { + val editor = context.getString(R.string.config_screenshotEditor) + if (editor.isNotEmpty()) { + editIntent.component = ComponentName.unflattenFromString(editor) + } } return editIntent @@ -92,7 +104,7 @@ object ActionIntentCreator { } /** @return an Intent to start the LongScreenshotActivity */ - fun createLongScreenshotIntent(owner: UserHandle, context: Context): Intent { + fun createLongScreenshotIntent(owner: UserHandle): Intent { return Intent(context, LongScreenshotActivity::class.java) .putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, owner) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -100,8 +112,35 @@ object ActionIntentCreator { .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) } - private const val EXTRA_EDIT_SOURCE = "edit_source" - private const val EDIT_SOURCE_SCREENSHOT = "screenshot" + private fun preferredEditor(): ComponentName? = + runCatching { + val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor) + val component = ComponentName.unflattenFromString(preferredEditor) ?: return null + + val info = + packageManager.getPackageInfo( + component.packageName, + PackageManager.GET_ACTIVITIES, + ) + + return info.activities + ?.firstOrNull { it.componentName.className.equals(component.className) } + ?.componentName + } + .getOrNull() + + private fun defaultEditor(): ComponentName? = + runCatching { + context.getString(R.string.config_screenshotEditor).let { + ComponentName.unflattenFromString(it) + } + } + .getOrNull() + + companion object { + private const val EXTRA_EDIT_SOURCE = "edit_source" + private const val EDIT_SOURCE_SCREENSHOT = "screenshot" + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java index 2259b55dc268..621bdd14825e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java @@ -139,6 +139,7 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler private RequestCallback mCurrentRequestCallback; private String mPackageName = ""; private final BroadcastReceiver mCopyBroadcastReceiver; + private final ActionIntentCreator mActionIntentCreator; /** Tracks config changes that require re-creating UI */ private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( @@ -174,6 +175,7 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, AnnouncementResolver announcementResolver, + ActionIntentCreator actionIntentCreator, @Assisted Display display ) { mScreenshotSmartActions = screenshotSmartActions; @@ -201,6 +203,7 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; mAnnouncementResolver = announcementResolver; + mActionIntentCreator = actionIntentCreator; mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId()); @@ -509,8 +512,7 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { mScrollCaptureExecutor.executeBatchScrollCapture(response, () -> { - final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent( - owner, mContext); + final Intent intent = mActionIntentCreator.createLongScreenshotIntent(owner); mContext.startActivity(intent); }, mViewProxy::restoreNonScrollingUi, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index 3696b133000c..4373389f4a51 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -24,9 +24,6 @@ import androidx.appcompat.content.res.AppCompatResources import com.android.internal.logging.UiEventLogger 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.ActionIntentCreator.createShareWithText 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 @@ -70,6 +67,7 @@ class DefaultScreenshotActionsProvider constructor( private val context: Context, private val uiEventLogger: UiEventLogger, + private val actionIntentCreator: ActionIntentCreator, @Assisted val requestId: UUID, @Assisted val request: ScreenshotData, @Assisted val actionExecutor: ActionExecutor, @@ -88,7 +86,7 @@ constructor( uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString) onDeferrableActionTapped { result -> actionExecutor.startSharedTransition( - createEdit(result.uri, context), + actionIntentCreator.createEdit(result.uri), result.user, true, ) @@ -110,9 +108,12 @@ constructor( val uri = webUri val shareIntent = if (screenshotContextUrl() && uri != null) { - createShareWithText(result.uri, extraText = uri.toString()) + actionIntentCreator.createShareWithText( + result.uri, + extraText = uri.toString(), + ) } else { - createShareWithSubject(result.uri, result.subject) + actionIntentCreator.createShareWithSubject(result.uri, result.subject) } actionExecutor.startSharedTransition(shareIntent, result.user, false) } @@ -130,7 +131,7 @@ constructor( uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString) onDeferrableActionTapped { result -> actionExecutor.startSharedTransition( - createEdit(result.uri, context), + actionIntentCreator.createEdit(result.uri), result.user, true, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt index f5c605211520..ea02b075fe6b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt @@ -46,7 +46,6 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.clipboardoverlay.ClipboardOverlayController import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R -import com.android.systemui.screenshot.ActionIntentCreator.createLongScreenshotIntent import com.android.systemui.screenshot.ScreenshotShelfViewProxy.ScreenshotViewCallback import com.android.systemui.screenshot.scroll.ScrollCaptureController.LongScreenshot import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor @@ -84,6 +83,7 @@ internal constructor( private val messageContainerController: MessageContainerController, private val announcementResolver: AnnouncementResolver, @Main private val mainExecutor: Executor, + private val actionIntentCreator: ActionIntentCreator, @Assisted private val display: Display, ) : InteractiveScreenshotHandler { private val context: WindowContext @@ -408,7 +408,7 @@ internal constructor( scrollCaptureExecutor.executeBatchScrollCapture( response, { - val intent = createLongScreenshotIntent(owner, context) + val intent = actionIntentCreator.createLongScreenshotIntent(owner) context.startActivity(intent) }, { viewProxy.restoreNonScrollingUi() }, 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 b43a1d23da24..48e08a0496c1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -87,6 +87,7 @@ public class LongScreenshotActivity extends Activity { private final ImageExporter mImageExporter; private final LongScreenshotData mLongScreenshotHolder; private final ActionIntentExecutor mActionExecutor; + private final ActionIntentCreator mActionIntentCreator; private ImageView mPreview; private ImageView mTransitionView; @@ -117,13 +118,15 @@ public class LongScreenshotActivity extends Activity { @Inject public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor, - LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor) { + LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor, + ActionIntentCreator actionIntentCreator) { mUiEventLogger = uiEventLogger; mUiExecutor = mainExecutor; mBackgroundExecutor = bgExecutor; mImageExporter = imageExporter; mLongScreenshotHolder = longScreenshotHolder; mActionExecutor = actionExecutor; + mActionIntentCreator = actionIntentCreator; } @@ -348,7 +351,7 @@ public class LongScreenshotActivity extends Activity { if (mScreenshotUserHandle != Process.myUserHandle()) { // TODO: Fix transition for work profile. Omitting it in the meantime. mActionExecutor.launchIntentAsync( - ActionIntentCreator.INSTANCE.createEdit(uri, this), + mActionIntentCreator.createEdit(uri), mScreenshotUserHandle, false, /* activityOptions */ null, /* transitionCoordinator */ null); } else { @@ -376,7 +379,7 @@ public class LongScreenshotActivity extends Activity { } private void doShare(Uri uri) { - Intent shareIntent = ActionIntentCreator.INSTANCE.createShare(uri); + Intent shareIntent = mActionIntentCreator.createShare(uri); mActionExecutor.launchIntentAsync(shareIntent, mScreenshotUserHandle, false, /* activityOptions */ null, /* transitionCoordinator */ null); } 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 67eaf7b9d19e..600572545d55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -19,29 +19,39 @@ package com.android.systemui.screenshot import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.truth.content.IntentSubject.assertThat as assertThatIntent import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.shared.Flags 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 org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class ActionIntentCreatorTest : SysuiTestCase() { + val context = mock<Context>() + val packageManager = mock<PackageManager>() + private val actionIntentCreator = ActionIntentCreator(context, packageManager) @Test fun testCreateShare() { val uri = Uri.parse("content://fake") - val output = ActionIntentCreator.createShare(uri) + val output = actionIntentCreator.createShare(uri) assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) assertThatIntent(output) @@ -66,7 +76,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { fun testCreateShare_embeddedUserIdRemoved() { val uri = Uri.parse("content://555@fake") - val output = ActionIntentCreator.createShare(uri) + val output = actionIntentCreator.createShare(uri) assertThatIntent(output.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) .hasData(Uri.parse("content://fake")) @@ -77,7 +87,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { val uri = Uri.parse("content://fake") val subject = "Example subject" - val output = ActionIntentCreator.createShareWithSubject(uri, subject) + val output = actionIntentCreator.createShareWithSubject(uri, subject) assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) assertThatIntent(output) @@ -101,7 +111,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { val uri = Uri.parse("content://fake") val extraText = "Extra text" - val output = ActionIntentCreator.createShareWithText(uri, extraText) + val output = actionIntentCreator.createShareWithText(uri, extraText) assertThatIntent(output).hasAction(Intent.ACTION_CHOOSER) assertThatIntent(output) @@ -121,13 +131,61 @@ class ActionIntentCreatorTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEditLegacy() { + val uri = Uri.parse("content://fake") + + whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") + + val output = actionIntentCreator.createEdit(uri) + + assertThatIntent(output).hasAction(Intent.ACTION_EDIT) + assertThatIntent(output).hasData(uri) + assertThatIntent(output).hasType("image/png") + assertWithMessage("getComponent()").that(output.component).isNull() + assertThat(output.getStringExtra("edit_source")).isEqualTo("screenshot") + assertThatIntent(output) + .hasFlags( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK + ) + } + + @Test + @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEditLegacy_embeddedUserIdRemoved() { + val uri = Uri.parse("content://555@fake") + whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") + + val output = actionIntentCreator.createEdit(uri) + + assertThatIntent(output).hasData(Uri.parse("content://fake")) + } + + @Test + @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEditLegacy_withEditor() { + val uri = Uri.parse("content://fake") + val component = ComponentName("com.android.foo", "com.android.foo.Something") + + whenever(context.getString(eq(R.string.config_screenshotEditor))) + .thenReturn(component.flattenToString()) + + val output = actionIntentCreator.createEdit(uri) + + assertThatIntent(output).hasComponent(component) + } + + @Test + @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) fun testCreateEdit() { val uri = Uri.parse("content://fake") - val context = mock<Context>() whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") - val output = ActionIntentCreator.createEdit(uri, context) + val output = actionIntentCreator.createEdit(uri) assertThatIntent(output).hasAction(Intent.ACTION_EDIT) assertThatIntent(output).hasData(uri) @@ -144,26 +202,78 @@ class ActionIntentCreatorTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) fun testCreateEdit_embeddedUserIdRemoved() { val uri = Uri.parse("content://555@fake") - val context = mock<Context>() whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") - val output = ActionIntentCreator.createEdit(uri, context) + val output = actionIntentCreator.createEdit(uri) assertThatIntent(output).hasData(Uri.parse("content://fake")) } @Test - fun testCreateEdit_withEditor() { + @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEdit_withPreferredEditorEnabled() { + 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") + + val packageInfo = + PackageInfo().apply { + activities = + arrayOf( + ActivityInfo().apply { + packageName = preferredComponent.packageName + name = preferredComponent.className + } + ) + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + whenever(context.getString(eq(R.string.config_screenshotEditor))) + .thenReturn(fallbackComponent.flattenToString()) + whenever(context.getString(eq(R.string.config_preferredScreenshotEditor))) + .thenReturn(preferredComponent.flattenToString()) + + val output = actionIntentCreator.createEdit(uri) + + assertThatIntent(output).hasComponent(preferredComponent) + } + + @Test + @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEdit_withPreferredEditorDisabled() { + 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") + + val packageInfo = + PackageInfo().apply { + activities = arrayOf() // no activities + } + whenever(packageManager.getPackageInfo(eq(preferredComponent.packageName), anyInt())) + .thenReturn(packageInfo) + whenever(context.getString(eq(R.string.config_screenshotEditor))) + .thenReturn(fallbackComponent.flattenToString()) + whenever(context.getString(eq(R.string.config_preferredScreenshotEditor))) + .thenReturn(preferredComponent.flattenToString()) + + val output = actionIntentCreator.createEdit(uri) + + assertThatIntent(output).hasComponent(fallbackComponent) + } + + @Test + @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) + fun testCreateEdit_withFallbackEditor() { val uri = Uri.parse("content://fake") - val context = mock<Context>() val component = ComponentName("com.android.foo", "com.android.foo.Something") whenever(context.getString(eq(R.string.config_screenshotEditor))) .thenReturn(component.flattenToString()) - val output = ActionIntentCreator.createEdit(uri, context) + val output = actionIntentCreator.createEdit(uri) assertThatIntent(output).hasComponent(component) } |