summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ajinkya Chalke <achalke@google.com> 2024-06-20 12:54:26 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-06-20 12:54:26 +0000
commitcf10a2b180bbb295d4def030d3e4e957f7d49da0 (patch)
tree9fc5123831d1fb48acb8a8dcfe55b6be6142c4c5
parent3a02a369e7ca8d5d358182e328a0e01b28ce6bbd (diff)
parentc795b8f683080a8b36064bd201b04086eccce89b (diff)
Merge "Implements basic version of Backlinks" into main
-rw-r--r--packages/SystemUI/res/layout/app_clips_screenshot.xml9
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java37
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java220
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java104
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java260
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;
+ }
}