diff options
3 files changed, 196 insertions, 65 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 9a38358a2768..4b1504f1cc2f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -28,9 +28,9 @@ import android.app.TaskInfo; import android.app.WindowConfiguration; import android.app.assist.AssistContent; import android.content.ClipData; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; @@ -249,7 +249,6 @@ final class AppClipsViewModel extends ViewModel { .map(taskInfo -> new InternalTaskInfo(taskInfo.topActivityInfo, taskInfo.taskId, taskInfo.userId, getPackageManagerForUser(taskInfo.userId))) - .filter(this::canAppStartThroughLauncher) .map(this::getBacklinksDataForTaskInfo) .toList(), mBgExecutor); @@ -257,6 +256,17 @@ final class AppClipsViewModel extends ViewModel { return Futures.transformAsync(backlinksNestedListFuture, Futures::allAsList, mBgExecutor); } + private PackageManager getPackageManagerForUser(int userId) { + // If app clips was launched as the same user, then reuse the available PM from mContext. + if (mContext.getUserId() == userId) { + return mContext.getPackageManager(); + } + + // PackageManager required for a different user, create its context and return its PM. + UserHandle userHandle = UserHandle.of(userId); + return mContext.createContextAsUser(userHandle, /* flags= */ 0).getPackageManager(); + } + /** * Returns all tasks on a given display after querying {@link IActivityTaskManager} from the * {@link #mBgExecutor}. @@ -311,17 +321,6 @@ final class AppClipsViewModel extends ViewModel { } /** - * Returns whether the app represented by the {@link InternalTaskInfo} can be launched through - * the all apps tray by a user. - */ - private boolean canAppStartThroughLauncher(InternalTaskInfo internalTaskInfo) { - // Use Intent.resolveActivity API to check if the intent resolves as that is what Android - // uses internally when apps use Context.startActivity. - return getMainLauncherIntentForTask(internalTaskInfo) - .resolveActivity(internalTaskInfo.getPackageManager()) != null; - } - - /** * Returns an {@link InternalBacklinksData} that represents the Backlink data internally, which * is captured by querying the system using {@link TaskInfo#taskId}. */ @@ -390,11 +389,14 @@ final class AppClipsViewModel extends ViewModel { internalTaskInfo.getTaskId(), internalTaskInfo.getTopActivityNameForDebugLogging())); - String appName = internalTaskInfo.getTopActivityAppName(); - Drawable appIcon = internalTaskInfo.getTopActivityAppIcon(); - ClipData mainLauncherIntent = ClipData.newIntent(appName, - getMainLauncherIntentForTask(internalTaskInfo)); - InternalBacklinksData fallback = new BacklinksData(mainLauncherIntent, appIcon); + String screenshottedAppName = internalTaskInfo.getTopActivityAppName(); + Drawable screenshottedAppIcon = internalTaskInfo.getTopActivityAppIcon(); + Intent screenshottedAppMainLauncherIntent = getMainLauncherIntentForTask( + internalTaskInfo.getTopActivityPackageName(), internalTaskInfo.getPackageManager()); + ClipData screenshottedAppMainLauncherClipData = + ClipData.newIntent(screenshottedAppName, screenshottedAppMainLauncherIntent); + InternalBacklinksData fallback = + new BacklinksData(screenshottedAppMainLauncherClipData, screenshottedAppIcon); if (content == null) { return fallback; } @@ -406,10 +408,14 @@ final class AppClipsViewModel extends ViewModel { Uri uri = content.getWebUri(); Intent backlinksIntent = new Intent(ACTION_VIEW).setData(uri); - if (doesIntentResolveToSameTask(backlinksIntent, internalTaskInfo)) { + BacklinkDisplayInfo backlinkDisplayInfo = getInfoThatResolvesIntent(backlinksIntent, + internalTaskInfo); + if (backlinkDisplayInfo != null) { DebugLogger.INSTANCE.logcatMessage(this, () -> "getBacklinksDataFromAssistContent: using app provided uri"); - return new BacklinksData(ClipData.newRawUri(appName, uri), appIcon); + return new BacklinksData( + ClipData.newRawUri(backlinkDisplayInfo.getDisplayLabel(), uri), + backlinkDisplayInfo.getAppIcon()); } } @@ -419,10 +425,14 @@ final class AppClipsViewModel extends ViewModel { () -> "getBacklinksDataFromAssistContent: app has provided an intent"); Intent backlinksIntent = content.getIntent(); - if (doesIntentResolveToSameTask(backlinksIntent, internalTaskInfo)) { + BacklinkDisplayInfo backlinkDisplayInfo = getInfoThatResolvesIntent(backlinksIntent, + internalTaskInfo); + if (backlinkDisplayInfo != null) { DebugLogger.INSTANCE.logcatMessage(this, () -> "getBacklinksDataFromAssistContent: using app provided intent"); - return new BacklinksData(ClipData.newIntent(appName, backlinksIntent), appIcon); + return new BacklinksData( + ClipData.newIntent(backlinkDisplayInfo.getDisplayLabel(), backlinksIntent), + backlinkDisplayInfo.getAppIcon()); } } @@ -431,27 +441,76 @@ final class AppClipsViewModel extends ViewModel { return fallback; } - private boolean doesIntentResolveToSameTask(Intent intentToResolve, - InternalTaskInfo requiredTaskInfo) { - PackageManager packageManager = requiredTaskInfo.getPackageManager(); - ComponentName resolvedComponent = intentToResolve.resolveActivity(packageManager); - if (resolvedComponent == null) { - return false; + /** + * Returns {@link BacklinkDisplayInfo} for the app that would resolve the provided backlink + * {@link Intent}. + * + * <p>The method uses the {@link PackageManager} available in the provided + * {@link InternalTaskInfo}. + * + * <p>This method returns {@code null} if Android is not able to resolve the backlink intent or + * if the resolved app does not have an icon in the launcher. + */ + @Nullable + private BacklinkDisplayInfo getInfoThatResolvesIntent(Intent backlinkIntent, + InternalTaskInfo internalTaskInfo) { + PackageManager packageManager = internalTaskInfo.getPackageManager(); + + // Query for all available activities as there is a chance that multiple apps could resolve + // the intent. In such cases the normal `intent.resolveActivity` API returns the activity + // resolver info which isn't helpful for further checks. Also, using MATCH_DEFAULT_ONLY flag + // is required as that flag will be used when the notes app builds the intent and calls + // startActivity with the intent. + List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(backlinkIntent, + PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfos.isEmpty()) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getInfoThatResolvesIntent: could not resolve backlink intent"); + return null; + } + + // Only use the first result as the list is ordered from best match to worst and Android + // will also use the best match with `intent.startActivity` API which notes app will use. + ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; + if (activityInfo == null) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getInfoThatResolvesIntent: could not find activity info for backlink " + + "intent"); + return null; + } + + // Ignore resolved backlink app if users cannot start it through all apps tray. + if (!canAppStartThroughLauncher(activityInfo.packageName, packageManager)) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getInfoThatResolvesIntent: ignoring resolved backlink app as it cannot" + + " start through launcher"); + return null; } - String requiredPackageName = requiredTaskInfo.getTopActivityPackageName(); - return resolvedComponent.getPackageName().equals(requiredPackageName); + Drawable appIcon = InternalBacklinksDataKt.getAppIcon(activityInfo, packageManager); + String appName = InternalBacklinksDataKt.getAppName(activityInfo, packageManager); + return new BacklinkDisplayInfo(appIcon, appName); + } + + /** + * Returns whether the app represented by the provided {@code pkgName} can be launched through + * the all apps tray by the user. + */ + private static boolean canAppStartThroughLauncher(String pkgName, PackageManager pkgManager) { + // Use Intent.resolveActivity API to check if the intent resolves as that is what Android + // uses internally when apps use Context.startActivity. + return getMainLauncherIntentForTask(pkgName, pkgManager) + .resolveActivity(pkgManager) != null; } - private Intent getMainLauncherIntentForTask(InternalTaskInfo internalTaskInfo) { - String pkgName = internalTaskInfo.getTopActivityPackageName(); + private static Intent getMainLauncherIntentForTask(String pkgName, + PackageManager packageManager) { Intent intent = new Intent(ACTION_MAIN).addCategory(CATEGORY_LAUNCHER).setPackage(pkgName); // Not all apps use DEFAULT_CATEGORY for their main launcher activity so the exact component // needs to be queried and set on the Intent in order for note-taking apps to be able to // start this intent. When starting an activity with an implicit intent, Android adds the // DEFAULT_CATEGORY flag otherwise it fails to resolve the intent. - PackageManager packageManager = internalTaskInfo.getPackageManager(); ResolveInfo resolvedActivity = packageManager.resolveActivity(intent, /* flags= */ 0); if (resolvedActivity != null) { intent.setComponent(resolvedActivity.getComponentInfo().getComponentName()); @@ -460,17 +519,6 @@ final class AppClipsViewModel extends ViewModel { return intent; } - private PackageManager getPackageManagerForUser(int userId) { - // If app clips was launched as the same user, then reuse the available PM from mContext. - if (mContext.getUserId() == userId) { - return mContext.getPackageManager(); - } - - // PackageManager required for a different user, create its context and return its PM. - UserHandle userHandle = UserHandle.of(userId); - return mContext.createContextAsUser(userHandle, /* flags= */ 0).getPackageManager(); - } - /** Helper factory to help with injecting {@link AppClipsViewModel}. */ static final class Factory implements ViewModelProvider.Factory { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt index 234692ea2fc6..f4faa36ef718 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt @@ -27,16 +27,27 @@ import android.graphics.drawable.Drawable * represent error when necessary. */ internal sealed class InternalBacklinksData( - open val appIcon: Drawable, - open var displayLabel: String + // Fields from this object are made accessible through accessors to keep call sites simpler. + private val backlinkDisplayInfo: BacklinkDisplayInfo, ) { - data class BacklinksData(val clipData: ClipData, override val appIcon: Drawable) : - InternalBacklinksData(appIcon, clipData.description.label.toString()) + // Use separate field to access display label so that callers don't have to access through + // internal object. + var displayLabel: String + get() = backlinkDisplayInfo.displayLabel + set(value) { + backlinkDisplayInfo.displayLabel = value + } - data class CrossProfileError( - override val appIcon: Drawable, - override var displayLabel: String - ) : InternalBacklinksData(appIcon, displayLabel) + // Use separate field to access app icon so that callers don't have to access through internal + // object. + val appIcon: Drawable + get() = backlinkDisplayInfo.appIcon + + data class BacklinksData(val clipData: ClipData, private val icon: Drawable) : + InternalBacklinksData(BacklinkDisplayInfo(icon, clipData.description.label.toString())) + + data class CrossProfileError(private val icon: Drawable, private var label: String) : + InternalBacklinksData(BacklinkDisplayInfo(icon, label)) } /** @@ -54,11 +65,16 @@ internal data class InternalTaskInfo( val userId: Int, val packageManager: PackageManager ) { - fun getTopActivityNameForDebugLogging(): String = topActivityInfo.name + val topActivityNameForDebugLogging: String = topActivityInfo.name + val topActivityPackageName: String = topActivityInfo.packageName + val topActivityAppName: String by lazy { topActivityInfo.getAppName(packageManager) } + val topActivityAppIcon: Drawable by lazy { topActivityInfo.loadIcon(packageManager) } +} - fun getTopActivityPackageName(): String = topActivityInfo.packageName +internal fun ActivityInfo.getAppName(packageManager: PackageManager) = + loadLabel(packageManager).toString() - fun getTopActivityAppName(): String = topActivityInfo.loadLabel(packageManager).toString() +internal fun ActivityInfo.getAppIcon(packageManager: PackageManager) = loadIcon(packageManager) - fun getTopActivityAppIcon(): Drawable = topActivityInfo.loadIcon(packageManager) -} +/** A class to hold data that is used for displaying backlink to user in SysUI activity. */ +internal data class BacklinkDisplayInfo(val appIcon: Drawable, var displayLabel: String) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index 5d71c054244a..886b32b09225 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -24,6 +24,7 @@ import static android.content.Intent.ACTION_MAIN; import static android.content.Intent.ACTION_VIEW; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; import static android.content.Intent.CATEGORY_LAUNCHER; +import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertThat; @@ -32,6 +33,7 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.reset; @@ -73,6 +75,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -108,19 +111,24 @@ public final class AppClipsViewModelTest extends SysuiTestCase { Context mMockedContext; @Mock private PackageManager mPackageManager; - private ArgumentCaptor<Intent> mPackageManagerIntentCaptor; + private ArgumentCaptor<Intent> mPackageManagerLauncherIntentCaptor; + private ArgumentCaptor<Intent> mPackageManagerBacklinkIntentCaptor; private AppClipsViewModel mViewModel; @Before public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); - mPackageManagerIntentCaptor = ArgumentCaptor.forClass(Intent.class); + mPackageManagerLauncherIntentCaptor = ArgumentCaptor.forClass(Intent.class); + mPackageManagerBacklinkIntentCaptor = ArgumentCaptor.forClass(Intent.class); // Set up mocking for backlinks. when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY)) .thenReturn(List.of(createTaskInfoForBacklinksTask())); - when(mPackageManager.resolveActivity(mPackageManagerIntentCaptor.capture(), anyInt())) - .thenReturn(createBacklinksTaskResolveInfo()); + ResolveInfo expectedResolveInfo = createBacklinksTaskResolveInfo(); + when(mPackageManager.resolveActivity(mPackageManagerLauncherIntentCaptor.capture(), + anyInt())).thenReturn(expectedResolveInfo); + when(mPackageManager.queryIntentActivities(mPackageManagerBacklinkIntentCaptor.capture(), + eq(MATCH_DEFAULT_ONLY))).thenReturn(List.of(expectedResolveInfo)); when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); when(mMockedContext.getPackageManager()).thenReturn(mPackageManager); @@ -209,7 +217,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); waitForIdleSync(); - Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + Intent queriedIntent = mPackageManagerBacklinkIntentCaptor.getValue(); assertThat(queriedIntent.getData()).isEqualTo(expectedUri); assertThat(queriedIntent.getAction()).isEqualTo(ACTION_VIEW); @@ -226,6 +234,63 @@ public final class AppClipsViewModelTest extends SysuiTestCase { } @Test + public void triggerBacklinks_shouldUpdateBacklinks_withUriForDifferentApp() { + Uri expectedUri = Uri.parse("https://android.com"); + AssistContent contentWithUri = new AssistContent(); + contentWithUri.setWebUri(expectedUri); + mockForAssistContent(contentWithUri, BACKLINKS_TASK_ID); + + // Reset PackageManager mocking done in setup. + reset(mPackageManager); + String package2 = BACKLINKS_TASK_PACKAGE_NAME + 2; + String appName2 = BACKLINKS_TASK_APP_NAME + 2; + ResolveInfo resolveInfo2 = createBacklinksTaskResolveInfo(); + ActivityInfo activityInfo2 = resolveInfo2.activityInfo; + activityInfo2.name = appName2; + activityInfo2.packageName = package2; + activityInfo2.applicationInfo.packageName = package2; + + Intent app2LauncherIntent = new Intent(ACTION_MAIN).addCategory( + CATEGORY_LAUNCHER).setPackage(package2); + when(mPackageManager.resolveActivity(intentEquals(app2LauncherIntent), eq(/* flags= */ 0))) + .thenReturn(resolveInfo2); + Intent uriIntent = new Intent(ACTION_VIEW).setData(expectedUri); + when(mPackageManager.queryIntentActivities(intentEquals(uriIntent), eq(MATCH_DEFAULT_ONLY))) + .thenReturn(List.of(resolveInfo2)); + when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue(); + ClipData clipData = result.getClipData(); + ClipDescription resultDescription = clipData.getDescription(); + assertThat(resultDescription.getLabel().toString()).isEqualTo(appName2); + assertThat(resultDescription.getMimeType(0)).isEqualTo(MIMETYPE_TEXT_URILIST); + assertThat(clipData.getItemCount()).isEqualTo(1); + assertThat(clipData.getItemAt(0).getUri()).isEqualTo(expectedUri); + + assertThat(mViewModel.getBacklinksLiveData().getValue().size()).isEqualTo(1); + } + + private static class IntentMatcher implements ArgumentMatcher<Intent> { + private final Intent mExpectedIntent; + + IntentMatcher(Intent expectedIntent) { + mExpectedIntent = expectedIntent; + } + + @Override + public boolean matches(Intent actualIntent) { + return actualIntent != null && mExpectedIntent.filterEquals(actualIntent); + } + } + + private static Intent intentEquals(Intent intent) { + return argThat(new IntentMatcher(intent)); + } + + @Test public void triggerBacklinks_withNonResolvableUri_usesMainLauncherIntent() { Uri expectedUri = Uri.parse("https://developers.android.com"); AssistContent contentWithUri = new AssistContent(); @@ -249,7 +314,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); waitForIdleSync(); - Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + Intent queriedIntent = mPackageManagerBacklinkIntentCaptor.getValue(); assertThat(queriedIntent.getPackage()).isEqualTo(expectedIntent.getPackage()); BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue(); @@ -283,7 +348,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); waitForIdleSync(); - Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + Intent queriedIntent = mPackageManagerLauncherIntentCaptor.getValue(); assertThat(queriedIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME); assertThat(queriedIntent.getAction()).isEqualTo(ACTION_MAIN); assertThat(queriedIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER); @@ -356,7 +421,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { // For each task, the logic queries PM 3 times, twice for verifying if an app can be // launched via launcher and once with the data provided in backlink intent. when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo1, - resolveInfo1, resolveInfo1, resolveInfo2, resolveInfo2, resolveInfo2); + resolveInfo1, resolveInfo2, resolveInfo2); + when(mPackageManager.queryIntentActivities(any(Intent.class), eq(MATCH_DEFAULT_ONLY))) + .thenReturn(List.of(resolveInfo1)).thenReturn(List.of(resolveInfo2)); when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY)) .thenReturn(List.of(runningTaskInfo1, runningTaskInfo2)); |