diff options
| author | 2024-06-20 12:54:26 +0000 | |
|---|---|---|
| committer | 2024-06-20 12:54:26 +0000 | |
| commit | cf10a2b180bbb295d4def030d3e4e957f7d49da0 (patch) | |
| tree | 9fc5123831d1fb48acb8a8dcfe55b6be6142c4c5 | |
| parent | 3a02a369e7ca8d5d358182e328a0e01b28ce6bbd (diff) | |
| parent | c795b8f683080a8b36064bd201b04086eccce89b (diff) | |
Merge "Implements basic version of Backlinks" into main
8 files changed, 612 insertions, 41 deletions
diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index bcc7bca8c915..a3af9490f585 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -51,6 +51,15 @@ app:layout_constraintStart_toEndOf="@id/save" app:layout_constraintTop_toTopOf="parent" /> + <TextView + android:id="@+id/backlinks_data" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/cancel" + 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 82dafc3ed1df..e13b803cc9ed 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -269,6 +269,8 @@ <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> + <!-- TODO(b/300307759): Temporary string for text view that displays backlinks data. [CHAR LIMIT=NONE] --> + <string name="backlinks_string" translatable="false">Open <xliff:g id="appName" example="Google Chrome">%1$s</xliff:g></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 d87d85b93b9d..59b47dc8d0ab 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -16,16 +16,20 @@ package com.android.systemui.screenshot.appclips; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_NAME; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_CALLING_PACKAGE_TASK_ID; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.PERMISSION_SELF; import android.app.Activity; import android.content.BroadcastReceiver; +import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -43,6 +47,7 @@ import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ImageView; +import android.widget.TextView; import androidx.activity.ComponentActivity; import androidx.annotation.Nullable; @@ -51,10 +56,13 @@ import androidx.lifecycle.ViewModelProvider; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger.UiEventEnum; import com.android.settingslib.Utils; +import com.android.systemui.Flags; import com.android.systemui.res.R; import com.android.systemui.screenshot.scroll.CropView; import com.android.systemui.settings.UserTracker; +import java.util.Set; + import javax.inject.Inject; /** @@ -73,8 +81,6 @@ import javax.inject.Inject; * * <p>This {@link Activity} runs in its own separate process to isolate memory intensive image * editing from SysUI process. - * - * TODO(b/267309532): Polish UI and animations. */ public class AppClipsActivity extends ComponentActivity { @@ -94,6 +100,7 @@ public class AppClipsActivity extends ComponentActivity { private CropView mCropView; private Button mSave; private Button mCancel; + private TextView mBacklinksData; private AppClipsViewModel mViewModel; private ResultReceiver mResultReceiver; @@ -153,11 +160,10 @@ public class AppClipsActivity extends ComponentActivity { mCancel = mLayout.findViewById(R.id.cancel); mSave.setOnClickListener(this::onClick); mCancel.setOnClickListener(this::onClick); - - mCropView = mLayout.findViewById(R.id.crop_view); - + mBacklinksData = mLayout.findViewById(R.id.backlinks_data); mPreview = mLayout.findViewById(R.id.preview); + mPreview.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> updateImageDimensions()); @@ -166,9 +172,19 @@ public class AppClipsActivity extends ComponentActivity { mViewModel.getScreenshot().observe(this, this::setScreenshot); mViewModel.getResultLiveData().observe(this, this::setResultThenFinish); mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish); + mViewModel.getBacklinksLiveData().observe(this, this::setBacklinksData); if (savedInstanceState == null) { - mViewModel.performScreenshot(); + int displayId = getDisplayId(); + mViewModel.performScreenshot(displayId); + + if (Flags.appClipsBacklinks()) { + int appClipsTaskId = getTaskId(); + int callingPackageTaskId = intent.getIntExtra(EXTRA_CALLING_PACKAGE_TASK_ID, + INVALID_TASK_ID); + Set<Integer> taskIdsToIgnore = Set.of(appClipsTaskId, callingPackageTaskId); + mViewModel.triggerBacklinks(taskIdsToIgnore, displayId); + } } } @@ -281,6 +297,15 @@ public class AppClipsActivity extends ComponentActivity { finish(); } + private void setBacklinksData(ClipData clipData) { + if (mBacklinksData.getVisibility() == View.GONE) { + mBacklinksData.setVisibility(View.VISIBLE); + } + + mBacklinksData.setText(String.format(getString(R.string.backlinks_string), + clipData.getDescription().getLabel())); + } + private void setError(int errorCode) { if (mResultReceiver == null) { return; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index 7de22b1a9c77..aaa5dfc01ba3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.UserHandle; +import android.util.Log; import androidx.annotation.Nullable; @@ -27,19 +28,18 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; -import com.android.systemui.settings.DisplayTracker; import javax.inject.Inject; /** An intermediary singleton object to help communicating with the cross process service. */ @SysUISingleton class AppClipsCrossProcessHelper { + private static final String TAG = AppClipsCrossProcessHelper.class.getSimpleName(); private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector; - private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context, DisplayTracker displayTracker) { + AppClipsCrossProcessHelper(@Application Context context) { // Start a service as main user so that even if the app clips activity is running as work // profile user the service is able to use correct instance of Bubbles to grab a screenshot // excluding the bubble layer. @@ -48,7 +48,6 @@ class AppClipsCrossProcessHelper { Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM, IAppClipsScreenshotHelperService.Stub::asInterface); - mDisplayTracker = displayTracker; } /** @@ -58,15 +57,16 @@ class AppClipsCrossProcessHelper { * pass around but not a {@link Bitmap}. */ @Nullable - Bitmap takeScreenshot() { + Bitmap takeScreenshot(int displayId) { try { AndroidFuture<ScreenshotHardwareBufferInternal> future = mProxyConnector.postForResult( - service -> - // Take a screenshot of the default display of the user. - service.takeScreenshot(mDisplayTracker.getDefaultDisplayId())); + service -> service.takeScreenshot(displayId)); return future.get().createBitmapThenCloseBuffer(); } catch (Exception e) { + Log.e(TAG, + String.format("Error while capturing a screenshot of displayId %d", displayId), + e); return null; } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index 48449b393903..3c4469d052b1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -85,6 +85,7 @@ public class AppClipsTrampolineActivity extends Activity { static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; + static final String EXTRA_CALLING_PACKAGE_TASK_ID = TAG + "CALLING_PACKAGE_TASK_ID"; private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0); private final NoteTaskController mNoteTaskController; @@ -193,12 +194,14 @@ public class AppClipsTrampolineActivity extends Activity { ComponentName componentName = ComponentName.unflattenFromString( getString(R.string.config_screenshotAppClipsActivityComponent)); String callingPackageName = getCallingPackage(); + int callingPackageTaskId = getTaskId(); Intent intent = new Intent() .setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) - .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); + .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName) + .putExtra(EXTRA_CALLING_PACKAGE_TASK_ID, callingPackageTaskId); try { startActivity(intent); 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 630d3380f4d7..9bb7bbfbe4c3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -16,9 +16,23 @@ package com.android.systemui.screenshot.appclips; +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 com.google.common.util.concurrent.Futures.withTimeout; + +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityTaskManager; +import android.app.WindowConfiguration; +import android.app.assist.AssistContent; +import android.content.ClipData; +import android.content.ComponentName; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; import android.graphics.RecordingCanvas; @@ -26,10 +40,13 @@ import android.graphics.Rect; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.RemoteException; import android.os.UserHandle; +import android.util.Log; import android.view.Display; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; @@ -37,22 +54,36 @@ import androidx.lifecycle.ViewModelProvider; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** A {@link ViewModel} to help with the App Clips screenshot flow. */ final class AppClipsViewModel extends ViewModel { + private static final String TAG = AppClipsViewModel.class.getSimpleName(); + private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; private final ImageExporter mImageExporter; + private final IActivityTaskManager mAtmService; + private final AssistContentRequester mAssistContentRequester; + private final PackageManager mPackageManager; + @Main private final Executor mMainExecutor; @Background @@ -61,24 +92,34 @@ final class AppClipsViewModel extends ViewModel { private final MutableLiveData<Bitmap> mScreenshotLiveData; private final MutableLiveData<Uri> mResultLiveData; private final MutableLiveData<Integer> mErrorLiveData; + private final MutableLiveData<ClipData> mBacklinksLiveData; - AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, - ImageExporter imageExporter, @Main Executor mainExecutor, - @Background Executor bgExecutor) { + private AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, + ImageExporter imageExporter, IActivityTaskManager atmService, + AssistContentRequester assistContentRequester, PackageManager packageManager, + @Main Executor mainExecutor, @Background Executor bgExecutor) { mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; mImageExporter = imageExporter; + mAtmService = atmService; + mAssistContentRequester = assistContentRequester; + mPackageManager = packageManager; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mScreenshotLiveData = new MutableLiveData<>(); mResultLiveData = new MutableLiveData<>(); mErrorLiveData = new MutableLiveData<>(); + mBacklinksLiveData = new MutableLiveData<>(); } - /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */ - void performScreenshot() { + /** + * Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link #getScreenshot()}. + * + * @param displayId id of the {@link Display} to capture screenshot. + */ + void performScreenshot(int displayId) { mBgExecutor.execute(() -> { - Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot(); + Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot(displayId); mMainExecutor.execute(() -> { if (screenshot == null) { mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED); @@ -89,6 +130,38 @@ final class AppClipsViewModel extends ViewModel { }); } + /** + * Triggers the Backlinks flow which: + * <ul> + * <li>Evaluates the task to query. + * <li>Requests {@link AssistContent} from that task. + * <li>Transforms the {@link AssistContent} into {@link ClipData} for Backlinks. + * <li>The {@link ClipData} is reported to activity via {@link #getBacklinksLiveData()}. + * </ul> + * + * @param taskIdsToIgnore id of the tasks to ignore when querying for {@link AssistContent} + * @param displayId id of the display to query tasks for Backlinks data + */ + void triggerBacklinks(Set<Integer> taskIdsToIgnore, int displayId) { + mBgExecutor.execute(() -> { + ListenableFuture<ClipData> backlinksData = getBacklinksData(taskIdsToIgnore, displayId); + Futures.addCallback(backlinksData, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable ClipData result) { + if (result != null) { + mBacklinksLiveData.setValue(result); + } + } + + @Override + public void onFailure(Throwable t) { + Log.e(TAG, "Error querying for Backlinks data", t); + } + }, mMainExecutor); + + }); + } + /** Returns a {@link LiveData} that holds the captured screenshot. */ LiveData<Bitmap> getScreenshot() { return mScreenshotLiveData; @@ -107,6 +180,11 @@ final class AppClipsViewModel extends ViewModel { return mErrorLiveData; } + /** Returns a {@link LiveData} that holds the Backlinks data in {@link ClipData}. */ + LiveData<ClipData> getBacklinksLiveData() { + return mBacklinksLiveData; + } + /** * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to * {@link LiveData}. @@ -148,21 +226,144 @@ final class AppClipsViewModel extends ViewModel { return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height()); } + private ListenableFuture<ClipData> getBacklinksData(Set<Integer> taskIdsToIgnore, + int displayId) { + return getAllRootTaskInfosOnDisplay(displayId) + .stream() + .filter(taskInfo -> shouldIncludeTask(taskInfo, taskIdsToIgnore)) + .findFirst() + .map(this::getBacklinksDataForTaskId) + .orElse(Futures.immediateFuture(null)); + } + + private List<RootTaskInfo> getAllRootTaskInfosOnDisplay(int displayId) { + try { + return mAtmService.getAllRootTaskInfosOnDisplay(displayId); + } catch (RemoteException e) { + Log.e(TAG, String.format("Error while querying for tasks on display %d", displayId), e); + return Collections.emptyList(); + } + } + + private boolean shouldIncludeTask(RootTaskInfo taskInfo, Set<Integer> taskIdsToIgnore) { + // 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. + return !taskIdsToIgnore.contains(taskInfo.taskId) + && taskInfo.isVisible + && taskInfo.isRunning + && taskInfo.numActivities > 0 + && taskInfo.topActivity != null + && taskInfo.topActivityInfo != null + && taskInfo.childTaskIds.length > 0 + && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD + && canAppStartThroughLauncher(taskInfo.topActivity.getPackageName()); + } + + private boolean canAppStartThroughLauncher(String packageName) { + return getMainLauncherIntentForPackage(packageName).resolveActivity(mPackageManager) + != null; + } + + private ListenableFuture<ClipData> getBacklinksDataForTaskId(RootTaskInfo taskInfo) { + SettableFuture<ClipData> backlinksData = SettableFuture.create(); + int taskId = taskInfo.taskId; + mAssistContentRequester.requestAssistContent(taskId, assistContent -> + backlinksData.set(getBacklinksDataFromAssistContent(taskInfo, assistContent))); + return withTimeout(backlinksData, 5L, TimeUnit.SECONDS, newSingleThreadScheduledExecutor()); + } + + /** + * A utility method to get {@link ClipData} to use for Backlinks functionality from + * {@link AssistContent} received from the app whose screenshot is taken. + * + * <p>There are multiple ways an app can provide deep-linkable data via {@link AssistContent} + * but Backlinks restricts to using only one way. The following is the ordered list based on + * preference: + * <ul> + * <li>{@link AssistContent#getWebUri()} is the most preferred way. + * <li>Second preference is given to {@link AssistContent#getIntent()} when the app provides + * the intent, see {@link AssistContent#isAppProvidedIntent()}. + * <li>The last preference is given to an {@link Intent} that is built using + * {@link Intent#ACTION_MAIN} and {@link Intent#CATEGORY_LAUNCHER}. + * </ul> + * + * @param taskInfo {@link RootTaskInfo} of the task which provided the {@link AssistContent}. + * @param content the {@link AssistContent} to map into Backlinks {@link ClipData}. + * @return {@link ClipData} that represents the Backlinks data. + */ + private ClipData getBacklinksDataFromAssistContent(RootTaskInfo taskInfo, + @Nullable AssistContent content) { + String appName = getAppNameOfTask(taskInfo); + String packageName = taskInfo.topActivity.getPackageName(); + ClipData fallback = ClipData.newIntent(appName, + getMainLauncherIntentForPackage(packageName)); + if (content == null) { + return fallback; + } + + // First preference is given to app provided uri. + if (content.isAppProvidedWebUri()) { + Uri uri = content.getWebUri(); + Intent backlinksIntent = new Intent(ACTION_VIEW).setData(uri); + if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + return ClipData.newRawUri(appName, uri); + } + } + + // Second preference is given to app provided, hopefully deep-linking, intent. + if (content.isAppProvidedIntent()) { + Intent backlinksIntent = content.getIntent(); + if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + return ClipData.newIntent(appName, backlinksIntent); + } + } + + return fallback; + } + + private boolean doesIntentResolveToSamePackage(Intent intentToResolve, + String requiredPackageName) { + ComponentName resolvedComponent = intentToResolve.resolveActivity(mPackageManager); + if (resolvedComponent == null) { + return false; + } + + return resolvedComponent.getPackageName().equals(requiredPackageName); + } + + private String getAppNameOfTask(RootTaskInfo taskInfo) { + return taskInfo.topActivityInfo.loadLabel(mPackageManager).toString(); + } + + private Intent getMainLauncherIntentForPackage(String packageName) { + return new Intent(ACTION_MAIN) + .addCategory(CATEGORY_LAUNCHER) + .setPackage(packageName); + } + /** Helper factory to help with injecting {@link AppClipsViewModel}. */ static final class Factory implements ViewModelProvider.Factory { private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; private final ImageExporter mImageExporter; + private final IActivityTaskManager mAtmService; + private final AssistContentRequester mAssistContentRequester; + private final PackageManager mPackageManager; @Main private final Executor mMainExecutor; @Background private final Executor mBgExecutor; @Inject - Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, - @Main Executor mainExecutor, @Background Executor bgExecutor) { + Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, + IActivityTaskManager atmService, AssistContentRequester assistContentRequester, + PackageManager packageManager, @Main Executor mainExecutor, + @Background Executor bgExecutor) { mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; mImageExporter = imageExporter; + mAtmService = atmService; + mAssistContentRequester = assistContentRequester; + mPackageManager = packageManager; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; } @@ -176,7 +377,8 @@ final class AppClipsViewModel extends ViewModel { //noinspection unchecked return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter, - mMainExecutor, mBgExecutor); + mAtmService, mAssistContentRequester, mPackageManager, mMainExecutor, + mBgExecutor); } } } 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 68a689321219..2981590a3037 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 @@ -17,6 +17,7 @@ package com.android.systemui.screenshot.appclips; import static android.app.Activity.RESULT_OK; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_CANCELLED; @@ -25,30 +26,45 @@ 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.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityTaskManager; +import android.app.assist.AssistContent; +import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Process; +import android.os.RemoteException; import android.os.ResultReceiver; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.view.Display; +import android.view.View; import android.widget.ImageView; +import android.widget.TextView; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.intercepting.SingleActivityFactory; import com.android.internal.logging.UiEventLogger; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; +import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; import com.android.systemui.settings.UserTracker; @@ -60,9 +76,11 @@ import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; import java.util.UUID; import java.util.concurrent.Executor; import java.util.function.BiConsumer; @@ -75,14 +93,27 @@ public final class AppClipsActivityTest extends SysuiTestCase { private static final Bitmap TEST_BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888); private static final String TEST_URI_STRING = "www.test-uri.com"; private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING); - private static final BiConsumer<Integer, Bundle> FAKE_CONSUMER = (unUsed1, unUsed2) -> {}; + private static final BiConsumer<Integer, Bundle> FAKE_CONSUMER = (unUsed1, unUsed2) -> { + }; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; + private static final int BACKLINKS_TASK_ID = 42; + private static final String BACKLINKS_TASK_APP_NAME = "Backlinks app"; + private static final String BACKLINKS_TASK_PACKAGE_NAME = "backlinksTaskPackageName"; + private static final RootTaskInfo TASK_THAT_SUPPORTS_BACKLINKS = + createTaskInfoForBacklinksTask(); + + private static final AssistContent ASSIST_CONTENT_FOR_BACKLINKS_TASK = + createAssistContentForBacklinksTask(); @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; @Mock private ImageExporter mImageExporter; @Mock + private IActivityTaskManager mAtmService; + @Mock + private AssistContentRequester mAssistContentRequester; + @Mock private PackageManager mPackageManager; @Mock private UserTracker mUserTracker; @@ -98,9 +129,10 @@ public final class AppClipsActivityTest extends SysuiTestCase { protected AppClipsActivityTestable create(Intent unUsed) { return new AppClipsActivityTestable( new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, - mImageExporter, getContext().getMainExecutor(), - directExecutor()), mPackageManager, mUserTracker, - mUiEventLogger); + mImageExporter, mAtmService, mAssistContentRequester, + mPackageManager, getContext().getMainExecutor(), + directExecutor()), + mPackageManager, mUserTracker, mUiEventLogger); } }; @@ -118,7 +150,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), any(ApplicationInfoFlags.class), eq(TEST_USER_ID))).thenReturn(applicationInfo); - when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(TEST_BITMAP); + when(mAppClipsCrossProcessHelper.takeScreenshot(anyInt())).thenReturn(TEST_BITMAP); ImageExporter.Result result = new ImageExporter.Result(); result.uri = TEST_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), @@ -132,10 +164,13 @@ public final class AppClipsActivityTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_APP_CLIPS_BACKLINKS) public void appClipsLaunched_screenshotDisplayed() { launchActivity(); assertThat(((ImageView) mActivity.findViewById(R.id.preview)).getDrawable()).isNotNull(); + assertThat(mActivity.findViewById(R.id.backlinks_data).getVisibility()) + .isEqualTo(View.GONE); } @Test @@ -176,6 +211,32 @@ public final class AppClipsActivityTest extends SysuiTestCase { verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_CANCELLED, TEST_UID, TEST_CALLING_PACKAGE); } + @Test + @EnableFlags(Flags.FLAG_APP_CLIPS_BACKLINKS) + public void appClipsLaunched_backlinks_displayed() throws RemoteException { + // Set up mocking to verify backlinks view is displayed on screen. + ArgumentCaptor<Integer> displayIdCaptor = ArgumentCaptor.forClass(Integer.class); + when(mAtmService.getAllRootTaskInfosOnDisplay(displayIdCaptor.capture())) + .thenReturn(List.of(TASK_THAT_SUPPORTS_BACKLINKS)); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(ASSIST_CONTENT_FOR_BACKLINKS_TASK); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + when(mPackageManager + .resolveActivity(any(Intent.class), anyInt())) + .thenReturn(createBacklinksTaskResolveInfo()); + + launchActivity(); + waitForIdleSync(); + + assertThat(displayIdCaptor.getValue()).isEqualTo(mActivity.getDisplayId()); + TextView backlinksData = mActivity.findViewById(R.id.backlinks_data); + assertThat(backlinksData.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(backlinksData.getText().toString()).isEqualTo( + mActivity.getString(R.string.backlinks_string, BACKLINKS_TASK_APP_NAME)); + } + private void launchActivity() { launchActivity(createResultReceiver(FAKE_CONSUMER)); } @@ -203,7 +264,7 @@ public final class AppClipsActivityTest extends SysuiTestCase { testReceiver.writeToParcel(parcel, 0); parcel.setDataPosition(0); - testReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); + testReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); parcel.recycle(); return testReceiver; } @@ -212,6 +273,37 @@ public final class AppClipsActivityTest extends SysuiTestCase { mContext.getMainExecutor().execute(runnable); } + private static ResolveInfo createBacklinksTaskResolveInfo() { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.applicationInfo = new ApplicationInfo(); + activityInfo.name = BACKLINKS_TASK_APP_NAME; + activityInfo.packageName = BACKLINKS_TASK_PACKAGE_NAME; + activityInfo.applicationInfo.packageName = BACKLINKS_TASK_PACKAGE_NAME; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + return resolveInfo; + } + + private static RootTaskInfo createTaskInfoForBacklinksTask() { + RootTaskInfo taskInfo = new RootTaskInfo(); + taskInfo.taskId = BACKLINKS_TASK_ID; + taskInfo.isVisible = true; + taskInfo.isRunning = true; + taskInfo.numActivities = 1; + taskInfo.topActivity = new ComponentName(BACKLINKS_TASK_PACKAGE_NAME, "backlinksClass"); + taskInfo.topActivityInfo = createBacklinksTaskResolveInfo().activityInfo; + taskInfo.baseIntent = new Intent().setComponent(taskInfo.topActivity); + taskInfo.childTaskIds = new int[]{BACKLINKS_TASK_ID + 1}; + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + return taskInfo; + } + + private static AssistContent createAssistContentForBacklinksTask() { + AssistContent content = new AssistContent(); + content.setWebUri(Uri.parse("https://developers.android.com")); + return content; + } + public static class AppClipsActivityTestable extends AppClipsActivity { public AppClipsActivityTestable(AppClipsViewModel.Factory viewModelFactory, 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 ad0797c88f62..dcb75d1e187b 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 @@ -16,28 +16,51 @@ package com.android.systemui.screenshot.appclips; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT; +import static android.content.ClipDescription.MIMETYPE_TEXT_URILIST; +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.view.Display.DEFAULT_DISPLAY; 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.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.ActivityTaskManager.RootTaskInfo; +import android.app.IActivityTaskManager; +import android.app.assist.AssistContent; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.net.Uri; import android.os.Process; +import android.os.RemoteException; import android.os.UserHandle; -import android.view.Display; import androidx.test.runner.AndroidJUnit4; import com.android.systemui.SysuiTestCase; +import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; import com.google.common.util.concurrent.Futures; @@ -45,9 +68,13 @@ import com.google.common.util.concurrent.Futures; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -60,27 +87,44 @@ public final class AppClipsViewModelTest extends SysuiTestCase { private static final Rect FAKE_RECT = new Rect(); private static final Uri FAKE_URI = Uri.parse("www.test-uri.com"); private static final UserHandle USER_HANDLE = Process.myUserHandle(); + private static final int BACKLINKS_TASK_ID = 42; + private static final String BACKLINKS_TASK_APP_NAME = "Ultimate question app"; + 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 PackageManager mPackageManager; + private ArgumentCaptor<Intent> mPackageManagerIntentCaptor; private AppClipsViewModel mViewModel; @Before - public void setUp() { + public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); + mPackageManagerIntentCaptor = ArgumentCaptor.forClass(Intent.class); + + // Set up mocking for backlinks. + when(mAtmService.getAllRootTaskInfosOnDisplay(DEFAULT_DISPLAY)) + .thenReturn(List.of(createTaskInfoForBacklinksTask())); + when(mPackageManager.resolveActivity(mPackageManagerIntentCaptor.capture(), anyInt())) + .thenReturn(createBacklinksTaskResolveInfo()); mViewModel = new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter, + mAtmService, mAssistContentRequester, mPackageManager, getContext().getMainExecutor(), directExecutor()).create(AppClipsViewModel.class); } @Test public void performScreenshot_fails_shouldUpdateErrorWithFailed() { - when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(null); + when(mAppClipsCrossProcessHelper.takeScreenshot(anyInt())).thenReturn(null); - mViewModel.performScreenshot(); + mViewModel.performScreenshot(DEFAULT_DISPLAY); waitForIdleSync(); - verify(mAppClipsCrossProcessHelper).takeScreenshot(); + verify(mAppClipsCrossProcessHelper).takeScreenshot(DEFAULT_DISPLAY); assertThat(mViewModel.getErrorLiveData().getValue()) .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); assertThat(mViewModel.getResultLiveData().getValue()).isNull(); @@ -88,12 +132,12 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void performScreenshot_succeeds_shouldUpdateScreenshotWithBitmap() { - when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(FAKE_BITMAP); + when(mAppClipsCrossProcessHelper.takeScreenshot(DEFAULT_DISPLAY)).thenReturn(FAKE_BITMAP); - mViewModel.performScreenshot(); + mViewModel.performScreenshot(DEFAULT_DISPLAY); waitForIdleSync(); - verify(mAppClipsCrossProcessHelper).takeScreenshot(); + verify(mAppClipsCrossProcessHelper).takeScreenshot(DEFAULT_DISPLAY); assertThat(mViewModel.getErrorLiveData().getValue()).isNull(); assertThat(mViewModel.getScreenshot().getValue()).isEqualTo(FAKE_BITMAP); } @@ -101,7 +145,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), eq(USER_HANDLE), - eq(Display.DEFAULT_DISPLAY))).thenReturn( + eq(DEFAULT_DISPLAY))).thenReturn( Futures.immediateFailedFuture(new ExecutionException(new Throwable()))); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); @@ -115,7 +159,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), eq(USER_HANDLE), - eq(Display.DEFAULT_DISPLAY))).thenReturn( + eq(DEFAULT_DISPLAY))).thenReturn( Futures.immediateFuture(new ImageExporter.Result())); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); @@ -131,7 +175,7 @@ public final class AppClipsViewModelTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = FAKE_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), eq(USER_HANDLE), - eq(Display.DEFAULT_DISPLAY))).thenReturn(Futures.immediateFuture(result)); + eq(DEFAULT_DISPLAY))).thenReturn(Futures.immediateFuture(result)); mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); @@ -139,4 +183,198 @@ public final class AppClipsViewModelTest extends SysuiTestCase { assertThat(mViewModel.getErrorLiveData().getValue()).isNull(); assertThat(mViewModel.getResultLiveData().getValue()).isEqualTo(FAKE_URI); } + + @Test + public void triggerBacklinks_shouldUpdateBacklinks_withUri() { + Uri expectedUri = Uri.parse("https://developers.android.com"); + AssistContent contentWithUri = new AssistContent(); + contentWithUri.setWebUri(expectedUri); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(contentWithUri); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + assertThat(queriedIntent.getData()).isEqualTo(expectedUri); + assertThat(queriedIntent.getAction()).isEqualTo(ACTION_VIEW); + + ClipData result = mViewModel.getBacklinksLiveData().getValue(); + ClipDescription resultDescription = result.getDescription(); + assertThat(resultDescription.getLabel().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); + assertThat(resultDescription.getMimeType(0)).isEqualTo(MIMETYPE_TEXT_URILIST); + assertThat(result.getItemCount()).isEqualTo(1); + assertThat(result.getItemAt(0).getUri()).isEqualTo(expectedUri); + } + + @Test + public void triggerBacklinks_withNonResolvableUri_usesMainLauncherIntent() { + Uri expectedUri = Uri.parse("https://developers.android.com"); + AssistContent contentWithUri = new AssistContent(); + contentWithUri.setWebUri(expectedUri); + resetPackageManagerMockingForUsingFallbackBacklinks(); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(contentWithUri); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + verifyMainLauncherBacklinksIntent(); + } + + @Test + public void triggerBacklinks_shouldUpdateBacklinks_withAppProvidedIntent() { + Intent expectedIntent = new Intent().setPackage(BACKLINKS_TASK_PACKAGE_NAME); + AssistContent contentWithAppProvidedIntent = new AssistContent(); + contentWithAppProvidedIntent.setIntent(expectedIntent); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(contentWithAppProvidedIntent); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + assertThat(queriedIntent.getPackage()).isEqualTo(expectedIntent.getPackage()); + + ClipData result = mViewModel.getBacklinksLiveData().getValue(); + ClipDescription resultDescription = result.getDescription(); + assertThat(resultDescription.getLabel().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); + assertThat(resultDescription.getMimeType(0)).isEqualTo(MIMETYPE_TEXT_INTENT); + assertThat(result.getItemCount()).isEqualTo(1); + assertThat(result.getItemAt(0).getIntent()).isEqualTo(expectedIntent); + } + + @Test + public void triggerBacklinks_withNonResolvableAppProvidedIntent_usesMainLauncherIntent() { + Intent expectedIntent = new Intent().setPackage(BACKLINKS_TASK_PACKAGE_NAME); + AssistContent contentWithAppProvidedIntent = new AssistContent(); + contentWithAppProvidedIntent.setIntent(expectedIntent); + resetPackageManagerMockingForUsingFallbackBacklinks(); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(contentWithAppProvidedIntent); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + verifyMainLauncherBacklinksIntent(); + } + + @Test + public void triggerBacklinks_shouldUpdateBacklinks_withMainLauncherIntent() { + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(EMPTY_ASSIST_CONTENT); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + Intent queriedIntent = mPackageManagerIntentCaptor.getValue(); + assertThat(queriedIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME); + assertThat(queriedIntent.getAction()).isEqualTo(ACTION_MAIN); + assertThat(queriedIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER); + + verifyMainLauncherBacklinksIntent(); + } + + @Test + public void triggerBacklinks_withNonResolvableMainLauncherIntent_noBacklinksAvailable() { + reset(mPackageManager); + doAnswer(invocation -> { + AssistContentRequester.Callback callback = invocation.getArgument(1); + callback.onAssistContentAvailable(EMPTY_ASSIST_CONTENT); + return null; + }).when(mAssistContentRequester).requestAssistContent(eq(BACKLINKS_TASK_ID), any()); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + assertThat(mViewModel.getBacklinksLiveData().getValue()).isNull(); + } + + @Test + public void triggerBacklinks_nonStandardActivityIgnored_noBacklinkAvailable() + throws RemoteException { + reset(mAtmService); + RootTaskInfo taskInfo = createTaskInfoForBacklinksTask(); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); + when(mAtmService.getAllRootTaskInfosOnDisplay(DEFAULT_DISPLAY)) + .thenReturn(List.of(taskInfo)); + + mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY); + waitForIdleSync(); + + assertThat(mViewModel.getBacklinksLiveData().getValue()).isNull(); + } + + @Test + public void triggerBacklinks_taskIdsToIgnoreConsidered_noBacklinkAvailable() { + mViewModel.triggerBacklinks(Set.of(BACKLINKS_TASK_ID), DEFAULT_DISPLAY); + waitForIdleSync(); + + assertThat(mViewModel.getBacklinksLiveData().getValue()).isNull(); + } + + private void resetPackageManagerMockingForUsingFallbackBacklinks() { + reset(mPackageManager); + when(mPackageManager.resolveActivity(any(Intent.class), anyInt())) + // First the logic queries whether a package has a launcher activity, this should + // resolve otherwise the logic filters out the task. + .thenReturn(createBacklinksTaskResolveInfo()) + // Then logic queries with the backlinks intent, this should not resolve for the + // logic to use the fallback intent. + .thenReturn(null); + } + + private void verifyMainLauncherBacklinksIntent() { + ClipData result = mViewModel.getBacklinksLiveData().getValue(); + assertThat(result.getItemCount()).isEqualTo(1); + + ClipDescription resultDescription = result.getDescription(); + assertThat(resultDescription.getLabel().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME); + assertThat(resultDescription.getMimeType(0)).isEqualTo(MIMETYPE_TEXT_INTENT); + + Intent actualBacklinksIntent = result.getItemAt(0).getIntent(); + assertThat(actualBacklinksIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME); + assertThat(actualBacklinksIntent.getAction()).isEqualTo(ACTION_MAIN); + assertThat(actualBacklinksIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER); + } + + private static ResolveInfo createBacklinksTaskResolveInfo() { + ActivityInfo activityInfo = new ActivityInfo(); + activityInfo.applicationInfo = new ApplicationInfo(); + activityInfo.name = BACKLINKS_TASK_APP_NAME; + activityInfo.packageName = BACKLINKS_TASK_PACKAGE_NAME; + activityInfo.applicationInfo.packageName = BACKLINKS_TASK_PACKAGE_NAME; + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = activityInfo; + return resolveInfo; + } + + private static RootTaskInfo createTaskInfoForBacklinksTask() { + RootTaskInfo taskInfo = new RootTaskInfo(); + taskInfo.taskId = BACKLINKS_TASK_ID; + taskInfo.isVisible = true; + taskInfo.isRunning = true; + taskInfo.numActivities = 1; + taskInfo.topActivity = new ComponentName(BACKLINKS_TASK_PACKAGE_NAME, "backlinksClass"); + taskInfo.topActivityInfo = createBacklinksTaskResolveInfo().activityInfo; + taskInfo.baseIntent = new Intent().setComponent(taskInfo.topActivity); + taskInfo.childTaskIds = new int[]{BACKLINKS_TASK_ID + 1}; + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + return taskInfo; + } } |