diff options
7 files changed, 323 insertions, 89 deletions
diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index d7b94ec015ac..7b7c96cb0322 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -82,6 +82,23 @@ app:layout_constraintStart_toEndOf="@id/backlinks_include_data" app:layout_constraintTop_toTopOf="parent" /> + <TextView + android:id="@+id/backlinks_cross_profile_error" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:drawablePadding="4dp" + android:drawableStart="@drawable/ic_info_outline" + android:drawableTint="?androidprv:attr/materialColorOnBackground" + android:gravity="center" + android:paddingHorizontal="8dp" + android:text="@string/backlinks_cross_profile_error" + android:textColor="?androidprv:attr/materialColorOnBackground" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/preview" + app:layout_constraintStart_toEndOf="@id/backlinks_data" + app:layout_constraintTop_toTopOf="parent" /> + <ImageView android:id="@+id/preview" android:layout_width="0px" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index be74291e705a..1fb1dad067f9 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -269,8 +269,12 @@ <string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string> <!-- Add to note button used in App Clips flow to return the saved screenshot image to notes app. [CHAR LIMIT=NONE] --> <string name="app_clips_save_add_to_note">Add to note</string> + <!-- A check box used in App Clips flow to return the captured backlink of the screenshotted app to notes app. [CHAR LIMIT=NONE] --> <string name="backlinks_include_link">Include link</string> + <!-- A label for backlinks app that is used if there are multiple backlinks with same app name. [CHAR LIMIT=NONE] --> <string name="backlinks_duplicate_label_format"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> <xliff:g id="frequencyCount" example="(1)">(%2$d)</xliff:g></string> + <!-- An error message to inform user that capturing backlink from cross profile apps is not possible. [CHAR LIMIT=NONE] --> + <string name="backlinks_cross_profile_error">Links can\'t be added from other profiles</string> <!-- Notification title displayed for screen recording [CHAR LIMIT=50]--> <string name="screenrecord_title">Screen Recorder</string> diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index ad5e772934c8..15a1d1d61626 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -68,6 +68,8 @@ import com.android.settingslib.Utils; import com.android.systemui.Flags; import com.android.systemui.log.DebugLogger; import com.android.systemui.res.R; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.BacklinksData; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.CrossProfileError; import com.android.systemui.screenshot.scroll.CropView; import com.android.systemui.settings.UserTracker; @@ -100,6 +102,7 @@ public class AppClipsActivity extends ComponentActivity { private static final String TAG = AppClipsActivity.class.getSimpleName(); private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0); private static final int DRAWABLE_END = 2; + private static final float DISABLE_ALPHA = 0.5f; private final AppClipsViewModel.Factory mViewModelFactory; private final PackageManager mPackageManager; @@ -116,6 +119,7 @@ public class AppClipsActivity extends ComponentActivity { private Button mCancel; private CheckBox mBacklinksIncludeDataCheckBox; private TextView mBacklinksDataTextView; + private TextView mBacklinksCrossProfileError; private AppClipsViewModel mViewModel; private ResultReceiver mResultReceiver; @@ -192,8 +196,8 @@ public class AppClipsActivity extends ComponentActivity { mBacklinksDataTextView = mLayout.findViewById(R.id.backlinks_data); mBacklinksIncludeDataCheckBox = mLayout.findViewById(R.id.backlinks_include_data); mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener( - (buttonView, isChecked) -> - mBacklinksDataTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE)); + this::backlinksIncludeDataCheckBoxCheckedChangeListener); + mBacklinksCrossProfileError = mLayout.findViewById(R.id.backlinks_cross_profile_error); mViewModel = new ViewModelProvider(this, mViewModelFactory).get(AppClipsViewModel.class); mViewModel.getScreenshot().observe(this, this::setScreenshot); @@ -312,10 +316,11 @@ public class AppClipsActivity extends ComponentActivity { Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS); data.putParcelable(EXTRA_SCREENSHOT_URI, uri); + InternalBacklinksData selectedBacklink = mViewModel.mSelectedBacklinksLiveData.getValue(); if (mBacklinksIncludeDataCheckBox.getVisibility() == View.VISIBLE && mBacklinksIncludeDataCheckBox.isChecked() - && mViewModel.mSelectedBacklinksLiveData.getValue() != null) { - ClipData backlinksData = mViewModel.mSelectedBacklinksLiveData.getValue().getClipData(); + && selectedBacklink instanceof BacklinksData) { + ClipData backlinksData = ((BacklinksData) selectedBacklink).getClipData(); data.putParcelable(EXTRA_CLIP_DATA, backlinksData); DebugLogger.INSTANCE.logcatMessage(this, @@ -459,6 +464,38 @@ public class AppClipsActivity extends ComponentActivity { mBacklinksDataTextView.setCompoundDrawablesRelative(/* start= */ appIcon, /* top= */ null, /* end= */ dropDownIcon, /* bottom= */ null); + + updateViewsToShowOrHideBacklinkError(backlinksData); + } + + /** Updates views to show or hide error with backlink. */ + private void updateViewsToShowOrHideBacklinkError(InternalBacklinksData backlinksData) { + // Remove the check box change listener before updating it to avoid updating backlink text + // view visibility. + mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener(null); + if (backlinksData instanceof CrossProfileError) { + // There's error with the backlink, unselect the checkbox and disable it. + mBacklinksIncludeDataCheckBox.setEnabled(false); + mBacklinksIncludeDataCheckBox.setChecked(false); + mBacklinksIncludeDataCheckBox.setAlpha(DISABLE_ALPHA); + + mBacklinksCrossProfileError.setVisibility(View.VISIBLE); + } else { + // When there is no error, ensure the check box is enabled and checked. + mBacklinksIncludeDataCheckBox.setEnabled(true); + mBacklinksIncludeDataCheckBox.setChecked(true); + mBacklinksIncludeDataCheckBox.setAlpha(1.0f); + + mBacklinksCrossProfileError.setVisibility(View.GONE); + } + + // (Re)Set the check box change listener as we're done making changes to the check box. + mBacklinksIncludeDataCheckBox.setOnCheckedChangeListener( + this::backlinksIncludeDataCheckBoxCheckedChangeListener); + } + + private void backlinksIncludeDataCheckBoxCheckedChangeListener(View unused, boolean isChecked) { + mBacklinksDataTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE); } private Rect createBacklinksTextViewDrawableBounds() { 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 3530b3ff0dfd..9a38358a2768 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -23,13 +23,13 @@ import static android.content.Intent.CATEGORY_LAUNCHER; import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; -import android.app.ActivityTaskManager.RootTaskInfo; import android.app.IActivityTaskManager; 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.PackageManager; import android.content.pm.ResolveInfo; @@ -51,11 +51,14 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.log.DebugLogger; import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.BacklinksData; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.CrossProfileError; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -82,7 +85,7 @@ final class AppClipsViewModel extends ViewModel { private final ImageExporter mImageExporter; private final IActivityTaskManager mAtmService; private final AssistContentRequester mAssistContentRequester; - private final PackageManager mPackageManager; + @Application private final Context mContext; @Main private final Executor mMainExecutor; @@ -97,13 +100,13 @@ final class AppClipsViewModel extends ViewModel { private AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, IActivityTaskManager atmService, - AssistContentRequester assistContentRequester, PackageManager packageManager, + AssistContentRequester assistContentRequester, @Application Context context, @Main Executor mainExecutor, @Background Executor bgExecutor) { mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; mImageExporter = imageExporter; mAtmService = atmService; mAssistContentRequester = assistContentRequester; - mPackageManager = packageManager; + mContext = context; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; @@ -243,6 +246,10 @@ final class AppClipsViewModel extends ViewModel { allTasksOnDisplay .stream() .filter(taskInfo -> shouldIncludeTask(taskInfo, taskIdsToIgnore)) + .map(taskInfo -> new InternalTaskInfo(taskInfo.topActivityInfo, + taskInfo.taskId, taskInfo.userId, + getPackageManagerForUser(taskInfo.userId))) + .filter(this::canAppStartThroughLauncher) .map(this::getBacklinksDataForTaskInfo) .toList(), mBgExecutor); @@ -284,33 +291,34 @@ final class AppClipsViewModel extends ViewModel { /** * Returns whether the app represented by the provided {@link TaskInfo} should be included for * querying for {@link AssistContent}. + * + * <p>This does not check whether the task has a launcher icon. */ private boolean shouldIncludeTask(TaskInfo taskInfo, Set<Integer> taskIdsToIgnore) { DebugLogger.INSTANCE.logcatMessage(this, () -> String.format("shouldIncludeTask taskId %d; topActivity %s", taskInfo.taskId, taskInfo.topActivity)); - // Only consider tasks that shouldn't be ignored, are visible, running, and have a launcher - // icon. Furthermore, types such as launcher/home/dock/assistant are ignored. + // Only consider tasks that shouldn't be ignored, are visible, and running. Furthermore, + // types such as launcher/home/dock/assistant are ignored. return !taskIdsToIgnore.contains(taskInfo.taskId) && taskInfo.isVisible && taskInfo.isRunning && taskInfo.numActivities > 0 && taskInfo.topActivity != null && taskInfo.topActivityInfo != null - && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD - && canAppStartThroughLauncher(taskInfo.topActivity.getPackageName()); + && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD; } /** - * Returns whether the app represented by the provided {@code packageName} can be launched - * through the all apps tray by a user. + * Returns whether the app represented by the {@link InternalTaskInfo} can be launched through + * the all apps tray by a user. */ - private boolean canAppStartThroughLauncher(String packageName) { + 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 getMainLauncherIntentForPackage(packageName).resolveActivity(mPackageManager) - != null; + return getMainLauncherIntentForTask(internalTaskInfo) + .resolveActivity(internalTaskInfo.getPackageManager()) != null; } /** @@ -318,18 +326,36 @@ final class AppClipsViewModel extends ViewModel { * is captured by querying the system using {@link TaskInfo#taskId}. */ private ListenableFuture<InternalBacklinksData> getBacklinksDataForTaskInfo( - TaskInfo taskInfo) { + InternalTaskInfo internalTaskInfo) { DebugLogger.INSTANCE.logcatMessage(this, () -> String.format("getBacklinksDataForTaskId for taskId %d; topActivity %s", - taskInfo.taskId, taskInfo.topActivity)); + internalTaskInfo.getTaskId(), + internalTaskInfo.getTopActivityNameForDebugLogging())); + + // Unlike other SysUI components, App Clips is started by the notes app so it runs as the + // same user as the notes app. That is, if the notes app was running as work profile user + // then App Clips also runs as work profile user. This is why while checking for user of the + // screenshotted app the check is performed using UserHandle.myUserId instead of using the + // more complex UserTracker. + if (internalTaskInfo.getUserId() != UserHandle.myUserId()) { + return getCrossProfileErrorBacklinkForTask(internalTaskInfo); + } SettableFuture<InternalBacklinksData> backlinksData = SettableFuture.create(); - int taskId = taskInfo.taskId; - mAssistContentRequester.requestAssistContent(taskId, assistContent -> - backlinksData.set(getBacklinksDataFromAssistContent(taskInfo, assistContent))); + int taskId = internalTaskInfo.getTaskId(); + mAssistContentRequester.requestAssistContent(taskId, assistContent -> backlinksData.set( + getBacklinksDataFromAssistContent(internalTaskInfo, assistContent))); return withTimeout(backlinksData); } + private ListenableFuture<InternalBacklinksData> getCrossProfileErrorBacklinkForTask( + InternalTaskInfo internalTaskInfo) { + String appName = internalTaskInfo.getTopActivityAppName(); + Drawable appIcon = internalTaskInfo.getTopActivityAppIcon(); + InternalBacklinksData errorData = new CrossProfileError(appIcon, appName); + return Futures.immediateFuture(errorData); + } + /** Returns the same {@link ListenableFuture} but with a 5 {@link TimeUnit#SECONDS} timeout. */ private static <V> ListenableFuture<V> withTimeout(ListenableFuture<V> future) { return Futures.withTimeout(future, 5L, TimeUnit.SECONDS, @@ -351,22 +377,24 @@ final class AppClipsViewModel extends ViewModel { * {@link Intent#ACTION_MAIN} and {@link Intent#CATEGORY_LAUNCHER}. * </ul> * - * @param taskInfo {@link RootTaskInfo} of the task which provided the {@link AssistContent}. + * @param internalTaskInfo {@link InternalTaskInfo} of the task which provided the + * {@link AssistContent}. * @param content the {@link AssistContent} to map into Backlinks {@link ClipData}. * @return {@link InternalBacklinksData} that represents the Backlinks data along with app icon. */ - private InternalBacklinksData getBacklinksDataFromAssistContent(TaskInfo taskInfo, + private InternalBacklinksData getBacklinksDataFromAssistContent( + InternalTaskInfo internalTaskInfo, @Nullable AssistContent content) { DebugLogger.INSTANCE.logcatMessage(this, () -> String.format("getBacklinksDataFromAssistContent taskId %d; topActivity %s", - taskInfo.taskId, taskInfo.topActivity)); + internalTaskInfo.getTaskId(), + internalTaskInfo.getTopActivityNameForDebugLogging())); - String appName = getAppNameOfTask(taskInfo); - String packageName = taskInfo.topActivity.getPackageName(); - Drawable appIcon = taskInfo.topActivityInfo.loadIcon(mPackageManager); + String appName = internalTaskInfo.getTopActivityAppName(); + Drawable appIcon = internalTaskInfo.getTopActivityAppIcon(); ClipData mainLauncherIntent = ClipData.newIntent(appName, - getMainLauncherIntentForPackage(packageName)); - InternalBacklinksData fallback = new InternalBacklinksData(mainLauncherIntent, appIcon); + getMainLauncherIntentForTask(internalTaskInfo)); + InternalBacklinksData fallback = new BacklinksData(mainLauncherIntent, appIcon); if (content == null) { return fallback; } @@ -378,10 +406,10 @@ final class AppClipsViewModel extends ViewModel { Uri uri = content.getWebUri(); Intent backlinksIntent = new Intent(ACTION_VIEW).setData(uri); - if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + if (doesIntentResolveToSameTask(backlinksIntent, internalTaskInfo)) { DebugLogger.INSTANCE.logcatMessage(this, () -> "getBacklinksDataFromAssistContent: using app provided uri"); - return new InternalBacklinksData(ClipData.newRawUri(appName, uri), appIcon); + return new BacklinksData(ClipData.newRawUri(appName, uri), appIcon); } } @@ -391,11 +419,10 @@ final class AppClipsViewModel extends ViewModel { () -> "getBacklinksDataFromAssistContent: app has provided an intent"); Intent backlinksIntent = content.getIntent(); - if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + if (doesIntentResolveToSameTask(backlinksIntent, internalTaskInfo)) { DebugLogger.INSTANCE.logcatMessage(this, () -> "getBacklinksDataFromAssistContent: using app provided intent"); - return new InternalBacklinksData(ClipData.newIntent(appName, backlinksIntent), - appIcon); + return new BacklinksData(ClipData.newIntent(appName, backlinksIntent), appIcon); } } @@ -404,28 +431,28 @@ final class AppClipsViewModel extends ViewModel { return fallback; } - private boolean doesIntentResolveToSamePackage(Intent intentToResolve, - String requiredPackageName) { - ComponentName resolvedComponent = intentToResolve.resolveActivity(mPackageManager); + private boolean doesIntentResolveToSameTask(Intent intentToResolve, + InternalTaskInfo requiredTaskInfo) { + PackageManager packageManager = requiredTaskInfo.getPackageManager(); + ComponentName resolvedComponent = intentToResolve.resolveActivity(packageManager); if (resolvedComponent == null) { return false; } + String requiredPackageName = requiredTaskInfo.getTopActivityPackageName(); return resolvedComponent.getPackageName().equals(requiredPackageName); } - private String getAppNameOfTask(TaskInfo taskInfo) { - return taskInfo.topActivityInfo.loadLabel(mPackageManager).toString(); - } - - private Intent getMainLauncherIntentForPackage(String pkgName) { + private Intent getMainLauncherIntentForTask(InternalTaskInfo internalTaskInfo) { + String pkgName = internalTaskInfo.getTopActivityPackageName(); 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. - ResolveInfo resolvedActivity = mPackageManager.resolveActivity(intent, /* flags= */ 0); + PackageManager packageManager = internalTaskInfo.getPackageManager(); + ResolveInfo resolvedActivity = packageManager.resolveActivity(intent, /* flags= */ 0); if (resolvedActivity != null) { intent.setComponent(resolvedActivity.getComponentInfo().getComponentName()); } @@ -433,6 +460,17 @@ 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 { @@ -440,7 +478,7 @@ final class AppClipsViewModel extends ViewModel { private final ImageExporter mImageExporter; private final IActivityTaskManager mAtmService; private final AssistContentRequester mAssistContentRequester; - private final PackageManager mPackageManager; + @Application private final Context mContext; @Main private final Executor mMainExecutor; @Background @@ -449,13 +487,13 @@ final class AppClipsViewModel extends ViewModel { @Inject Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, IActivityTaskManager atmService, AssistContentRequester assistContentRequester, - PackageManager packageManager, @Main Executor mainExecutor, + @Application Context context, @Main Executor mainExecutor, @Background Executor bgExecutor) { mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; mImageExporter = imageExporter; mAtmService = atmService; mAssistContentRequester = assistContentRequester; - mPackageManager = packageManager; + mContext = context; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; } @@ -469,7 +507,7 @@ final class AppClipsViewModel extends ViewModel { //noinspection unchecked return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter, - mAtmService, mAssistContentRequester, mPackageManager, mMainExecutor, + mAtmService, mAssistContentRequester, mContext, mMainExecutor, mBgExecutor); } } 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 30c33c5224ee..234692ea2fc6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/InternalBacklinksData.kt @@ -16,10 +16,49 @@ package com.android.systemui.screenshot.appclips +import android.app.TaskInfo import android.content.ClipData +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.graphics.drawable.Drawable -/** A class to hold the [ClipData] for backlinks and the corresponding app's [Drawable] icon. */ -internal data class InternalBacklinksData(val clipData: ClipData, val appIcon: Drawable) { - var displayLabel: String = clipData.description.label.toString() +/** + * A class to hold the [ClipData] for backlinks, the corresponding app's [Drawable] icon, and + * represent error when necessary. + */ +internal sealed class InternalBacklinksData( + open val appIcon: Drawable, + open var displayLabel: String +) { + data class BacklinksData(val clipData: ClipData, override val appIcon: Drawable) : + InternalBacklinksData(appIcon, clipData.description.label.toString()) + + data class CrossProfileError( + override val appIcon: Drawable, + override var displayLabel: String + ) : InternalBacklinksData(appIcon, displayLabel) +} + +/** + * A class to hold important members of [TaskInfo] and its associated user's [PackageManager] for + * ease of querying. + * + * @note A task can have a different app running on top. For example, an app "A" can use camera app + * to capture an image. In this case the top app will be the camera app even though the task + * belongs to app A. This is expected behaviour because user will be taking a screenshot of the + * content rendered by the camera (top) app. + */ +internal data class InternalTaskInfo( + private val topActivityInfo: ActivityInfo, + val taskId: Int, + val userId: Int, + val packageManager: PackageManager +) { + fun getTopActivityNameForDebugLogging(): String = topActivityInfo.name + + fun getTopActivityPackageName(): String = topActivityInfo.packageName + + fun getTopActivityAppName(): String = topActivityInfo.loadLabel(packageManager).toString() + + fun getTopActivityAppIcon(): Drawable = topActivityInfo.loadIcon(packageManager) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index eb1a04d8e4ff..e1cd5e420f66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -30,12 +30,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.app.IActivityTaskManager; import android.app.assist.AssistContent; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -51,6 +54,7 @@ import android.os.Parcel; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; @@ -120,6 +124,8 @@ public final class AppClipsActivityTest extends SysuiTestCase { @Mock private AssistContentRequester mAssistContentRequester; @Mock + private Context mMockedContext; + @Mock private PackageManager mPackageManager; @Mock private UserTracker mUserTracker; @@ -127,6 +133,9 @@ public final class AppClipsActivityTest extends SysuiTestCase { private UiEventLogger mUiEventLogger; private AppClipsActivity mActivity; + private TextView mBacklinksDataTextView; + private CheckBox mBacklinksIncludeDataCheckBox; + private TextView mBacklinksCrossProfileErrorTextView; // Using the deprecated ActivityTestRule and SingleActivityFactory to help with injecting mocks. private final SingleActivityFactory<AppClipsActivityTestable> mFactory = @@ -136,7 +145,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { return new AppClipsActivityTestable( new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter, mAtmService, mAssistContentRequester, - mPackageManager, getContext().getMainExecutor(), + mMockedContext, getContext().getMainExecutor(), directExecutor()), mPackageManager, mUserTracker, mUiEventLogger); } @@ -162,6 +171,9 @@ public final class AppClipsActivityTest extends SysuiTestCase { when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), eq(Process.myUserHandle()), eq(Display.DEFAULT_DISPLAY))) .thenReturn(Futures.immediateFuture(result)); + + when(mMockedContext.getPackageManager()).thenReturn(mPackageManager); + when(mMockedContext.createContextAsUser(any(), anyInt())).thenReturn(mMockedContext); } @After @@ -175,10 +187,9 @@ public final class AppClipsActivityTest extends SysuiTestCase { launchActivity(); assertThat(((ImageView) mActivity.findViewById(R.id.preview)).getDrawable()).isNotNull(); - assertThat(mActivity.findViewById(R.id.backlinks_data).getVisibility()) - .isEqualTo(View.GONE); - assertThat(mActivity.findViewById(R.id.backlinks_include_data).getVisibility()) - .isEqualTo(View.GONE); + assertThat(mBacklinksDataTextView.getVisibility()).isEqualTo(View.GONE); + assertThat(mBacklinksIncludeDataCheckBox.getVisibility()).isEqualTo(View.GONE); + assertThat(mBacklinksCrossProfileErrorTextView.getVisibility()).isEqualTo(View.GONE); } @Test @@ -228,20 +239,23 @@ public final class AppClipsActivityTest extends SysuiTestCase { waitForIdleSync(); assertThat(mDisplayIdCaptor.getValue()).isEqualTo(mActivity.getDisplayId()); - TextView backlinksData = mActivity.findViewById(R.id.backlinks_data); - assertThat(backlinksData.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(backlinksData.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); - assertThat(backlinksData.getCompoundDrawablesRelative()[0]).isEqualTo(FAKE_DRAWABLE); + assertThat(mBacklinksDataTextView.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mBacklinksDataTextView.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); + assertThat(mBacklinksDataTextView.getCompoundDrawablesRelative()[0]) + .isEqualTo(FAKE_DRAWABLE); // Verify dropdown icon is not shown and there are no click listeners on text view. - assertThat(backlinksData.getCompoundDrawablesRelative()[2]).isNull(); - assertThat(backlinksData.hasOnClickListeners()).isFalse(); + assertThat(mBacklinksDataTextView.getCompoundDrawablesRelative()[2]).isNull(); + assertThat(mBacklinksDataTextView.hasOnClickListeners()).isFalse(); - CheckBox backlinksIncludeData = mActivity.findViewById(R.id.backlinks_include_data); - assertThat(backlinksIncludeData.getVisibility()).isEqualTo(View.VISIBLE); - assertThat(backlinksIncludeData.getText().toString()) + assertThat(mBacklinksIncludeDataCheckBox.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mBacklinksIncludeDataCheckBox.getText().toString()) .isEqualTo(mActivity.getString(R.string.backlinks_include_link)); - assertThat(backlinksIncludeData.isChecked()).isTrue(); + assertThat(mBacklinksIncludeDataCheckBox.isChecked()).isTrue(); + + assertThat(mBacklinksIncludeDataCheckBox.getAlpha()).isEqualTo(1.0f); + assertThat(mBacklinksIncludeDataCheckBox.isEnabled()).isTrue(); + assertThat(mBacklinksCrossProfileErrorTextView.getVisibility()).isEqualTo(View.GONE); } @Test @@ -258,8 +272,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { assertThat(backlinksIncludeData.getVisibility()).isEqualTo(View.VISIBLE); assertThat(backlinksIncludeData.isChecked()).isFalse(); - TextView backlinksData = mActivity.findViewById(R.id.backlinks_data); - assertThat(backlinksData.getVisibility()).isEqualTo(View.GONE); + assertThat(mBacklinksDataTextView.getVisibility()).isEqualTo(View.GONE); } @Test @@ -300,12 +313,11 @@ public final class AppClipsActivityTest extends SysuiTestCase { waitForIdleSync(); // Verify default backlink shown to user and text view has on click listener. - TextView backlinksData = mActivity.findViewById(R.id.backlinks_data); - assertThat(backlinksData.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); - assertThat(backlinksData.hasOnClickListeners()).isTrue(); + assertThat(mBacklinksDataTextView.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); + assertThat(mBacklinksDataTextView.hasOnClickListeners()).isTrue(); // Verify dropdown icon is not null. - assertThat(backlinksData.getCompoundDrawablesRelative()[2]).isNotNull(); + assertThat(mBacklinksDataTextView.getCompoundDrawablesRelative()[2]).isNotNull(); } @Test @@ -336,12 +348,35 @@ public final class AppClipsActivityTest extends SysuiTestCase { waitForIdleSync(); // Verify default backlink shown to user has the numerical suffix. - TextView backlinksData = mActivity.findViewById(R.id.backlinks_data); - assertThat(backlinksData.getText().toString()).isEqualTo( - mContext.getString(R.string.backlinks_duplicate_label_format, + assertThat(mBacklinksDataTextView.getText().toString()).isEqualTo( + getContext().getString(R.string.backlinks_duplicate_label_format, BACKLINKS_TASK_APP_NAME, 1)); } + @Test + @EnableFlags(Flags.FLAG_APP_CLIPS_BACKLINKS) + public void appClipsLaunched_backlinks_singleBacklink_crossProfileError() + throws RemoteException { + // Set up mocking for cross profile backlink. + setUpMocksForBacklinks(); + ActivityManager.RunningTaskInfo crossProfileTaskInfo = createTaskInfoForBacklinksTask(); + crossProfileTaskInfo.userId = UserHandle.myUserId() + 1; + reset(mAtmService); + when(mAtmService.getTasks(eq(Integer.MAX_VALUE), eq(false), eq(false), + mDisplayIdCaptor.capture())).thenReturn(List.of(crossProfileTaskInfo)); + + // Trigger backlinks. + launchActivity(); + waitForIdleSync(); + + // Verify views for cross profile backlinks error. + assertThat(mBacklinksIncludeDataCheckBox.getAlpha()).isLessThan(1.0f); + assertThat(mBacklinksIncludeDataCheckBox.isEnabled()).isFalse(); + assertThat(mBacklinksIncludeDataCheckBox.isChecked()).isFalse(); + + assertThat(mBacklinksCrossProfileErrorTextView.getVisibility()).isEqualTo(View.VISIBLE); + } + private void setUpMocksForBacklinks() throws RemoteException { when(mAtmService.getTasks(eq(Integer.MAX_VALUE), eq(false), eq(false), mDisplayIdCaptor.capture())) @@ -373,11 +408,15 @@ public final class AppClipsActivityTest extends SysuiTestCase { mActivity = mActivityRule.launchActivity(intent); waitForIdleSync(); + mBacklinksDataTextView = mActivity.findViewById(R.id.backlinks_data); + mBacklinksIncludeDataCheckBox = mActivity.findViewById(R.id.backlinks_include_data); + mBacklinksCrossProfileErrorTextView = mActivity.findViewById( + R.id.backlinks_cross_profile_error); } private ResultReceiver createResultReceiver( BiConsumer<Integer, Bundle> resultReceiverConsumer) { - ResultReceiver testReceiver = new ResultReceiver(mContext.getMainThreadHandler()) { + ResultReceiver testReceiver = new ResultReceiver(getContext().getMainThreadHandler()) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { resultReceiverConsumer.accept(resultCode, resultData); @@ -394,7 +433,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { } private void runOnMainThread(Runnable runnable) { - mContext.getMainExecutor().execute(runnable); + getContext().getMainExecutor().execute(runnable); } private static ResolveInfo createBacklinksTaskResolveInfo() { @@ -418,6 +457,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { taskInfo.topActivityInfo = createBacklinksTaskResolveInfo().activityInfo; taskInfo.baseIntent = new Intent().setComponent(taskInfo.topActivity); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + taskInfo.userId = UserHandle.myUserId(); return taskInfo; } 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 178547e4ca3a..5d71c054244a 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 @@ -30,6 +30,7 @@ import static com.google.common.truth.Truth.assertThat; 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.eq; import static org.mockito.Mockito.doAnswer; @@ -43,6 +44,7 @@ import android.app.assist.AssistContent; import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -62,6 +64,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.BacklinksData; +import com.android.systemui.screenshot.appclips.InternalBacklinksData.CrossProfileError; import com.google.common.util.concurrent.Futures; @@ -92,10 +96,16 @@ public final class AppClipsViewModelTest extends SysuiTestCase { private static final String BACKLINKS_TASK_PACKAGE_NAME = "backlinksTaskPackageName"; private static final AssistContent EMPTY_ASSIST_CONTENT = new AssistContent(); - @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; - @Mock private ImageExporter mImageExporter; - @Mock private IActivityTaskManager mAtmService; - @Mock private AssistContentRequester mAssistContentRequester; + @Mock + private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; + @Mock + private ImageExporter mImageExporter; + @Mock + private IActivityTaskManager mAtmService; + @Mock + private AssistContentRequester mAssistContentRequester; + @Mock + Context mMockedContext; @Mock private PackageManager mPackageManager; private ArgumentCaptor<Intent> mPackageManagerIntentCaptor; @@ -112,10 +122,14 @@ public final class AppClipsViewModelTest extends SysuiTestCase { when(mPackageManager.resolveActivity(mPackageManagerIntentCaptor.capture(), anyInt())) .thenReturn(createBacklinksTaskResolveInfo()); when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE); + when(mMockedContext.getPackageManager()).thenReturn(mPackageManager); mViewModel = new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter, - mAtmService, mAssistContentRequester, mPackageManager, + mAtmService, mAssistContentRequester, mMockedContext, getContext().getMainExecutor(), directExecutor()).create(AppClipsViewModel.class); + + when(mMockedContext.getPackageManager()).thenReturn(mPackageManager); + when(mMockedContext.createContextAsUser(any(), anyInt())).thenReturn(mMockedContext); } @Test @@ -199,7 +213,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { assertThat(queriedIntent.getData()).isEqualTo(expectedUri); assertThat(queriedIntent.getAction()).isEqualTo(ACTION_VIEW); - InternalBacklinksData result = mViewModel.mSelectedBacklinksLiveData.getValue(); + BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue(); assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE); ClipData clipData = result.getClipData(); ClipDescription resultDescription = clipData.getDescription(); @@ -238,7 +252,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); assertThat(queriedIntent.getPackage()).isEqualTo(expectedIntent.getPackage()); - InternalBacklinksData result = mViewModel.mSelectedBacklinksLiveData.getValue(); + BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue(); assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE); ClipData clipData = result.getClipData(); ClipDescription resultDescription = clipData.getDescription(); @@ -307,6 +321,8 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void triggerBacklinks_taskIdsToIgnoreConsidered_noBacklinkAvailable() { + mockForAssistContent(EMPTY_ASSIST_CONTENT, BACKLINKS_TASK_ID); + mViewModel.triggerBacklinks(Set.of(BACKLINKS_TASK_ID), DEFAULT_DISPLAY); waitForIdleSync(); @@ -362,16 +378,58 @@ public final class AppClipsViewModelTest extends SysuiTestCase { waitForIdleSync(); // Verify two backlinks are received and the first backlink is set as default selected. - assertThat(mViewModel.mSelectedBacklinksLiveData.getValue().getClipData().getItemAt( - 0).getUri()).isEqualTo(expectedUri); + assertThat( + ((BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue()) + .getClipData().getItemAt(0).getUri()) + .isEqualTo(expectedUri); List<InternalBacklinksData> actualBacklinks = mViewModel.getBacklinksLiveData().getValue(); assertThat(actualBacklinks).hasSize(2); - assertThat(actualBacklinks.get(0).getClipData().getItemAt(0).getUri()) + assertThat(((BacklinksData) actualBacklinks.get(0)).getClipData().getItemAt(0).getUri()) .isEqualTo(expectedUri); - assertThat(actualBacklinks.get(1).getClipData().getItemAt(0).getIntent()) + assertThat(((BacklinksData) actualBacklinks.get(1)).getClipData().getItemAt(0).getIntent()) .isEqualTo(expectedIntent); } + @Test + public void triggerBacklinks_singleCrossProfileApp_shouldIndicateError() + throws RemoteException { + reset(mAtmService); + RunningTaskInfo taskInfo = createTaskInfoForBacklinksTask(); + taskInfo.userId = UserHandle.myUserId() + 1; + when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY)) + .thenReturn(List.of(taskInfo)); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + assertThat(mViewModel.mSelectedBacklinksLiveData.getValue()) + .isInstanceOf(CrossProfileError.class); + } + + @Test + public void triggerBacklinks_multipleBacklinks_includesCrossProfileError() + throws RemoteException { + // Set up mocking for multiple backlinks. + mockForAssistContent(EMPTY_ASSIST_CONTENT, BACKLINKS_TASK_ID); + reset(mAtmService); + RunningTaskInfo runningTaskInfo1 = createTaskInfoForBacklinksTask(); + RunningTaskInfo runningTaskInfo2 = createTaskInfoForBacklinksTask(); + runningTaskInfo2.userId = UserHandle.myUserId() + 1; + + when(mAtmService.getTasks(anyInt(), anyBoolean(), anyBoolean(), anyInt())) + .thenReturn(List.of(runningTaskInfo1, runningTaskInfo2)); + + // Set up complete, trigger the backlinks action. + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + // Verify two backlinks are received and only second has error. + List<InternalBacklinksData> actualBacklinks = mViewModel.getBacklinksLiveData().getValue(); + assertThat(actualBacklinks).hasSize(2); + assertThat(actualBacklinks.get(0)).isInstanceOf(BacklinksData.class); + assertThat(actualBacklinks.get(1)).isInstanceOf(CrossProfileError.class); + } + private void resetPackageManagerMockingForUsingFallbackBacklinks() { ResolveInfo backlinksTaskResolveInfo = createBacklinksTaskResolveInfo(); reset(mPackageManager); @@ -389,7 +447,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { } private void verifyMainLauncherBacklinksIntent() { - InternalBacklinksData result = mViewModel.mSelectedBacklinksLiveData.getValue(); + BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue(); assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE); ClipData clipData = result.getClipData(); @@ -436,6 +494,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { taskInfo.topActivityInfo = createBacklinksTaskResolveInfo().activityInfo; taskInfo.baseIntent = new Intent().setComponent(taskInfo.topActivity); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + taskInfo.userId = UserHandle.myUserId(); return taskInfo; } } |