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

Commit 923c3942 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Remove the backlink resolves to same app check" into main

parents 578a40d1 e77084fb
Loading
Loading
Loading
Loading
+92 −44
Original line number Diff line number Diff line
@@ -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}.
@@ -310,17 +320,6 @@ final class AppClipsViewModel extends ViewModel {
                && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD;
    }

    /**
     * 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 {

+29 −13
Original line number Diff line number Diff line
@@ -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
        }

    // 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(
        override val appIcon: Drawable,
        override var displayLabel: String
    ) : InternalBacklinksData(appIcon, displayLabel)
    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)
+75 −8
Original line number Diff line number Diff line
@@ -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);

@@ -225,6 +233,63 @@ public final class AppClipsViewModelTest extends SysuiTestCase {
        assertThat(result).isEqualTo(mViewModel.getBacklinksLiveData().getValue().get(0));
    }

    @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");
@@ -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));