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

Commit 4297e828 authored by Ajinkya Chalke's avatar Ajinkya Chalke
Browse files

Update Backlinks to capture data from all tasks

- In the previous version we were using ActivityManagerService's
  getAllRootTaskInfosOnDisplay API to get all the tasks on the display
  but this didn't return non-focused task from split screen as separate
  task which made it difficult to extract the non-focused tasks
  TaskInfo.
- The current getTasks API from the same service returns all tasks
  separately thus making it possible to get task info of non-focused
  task from split screen apps.

Bug: 300307759
Flag: com.android.systemui.app_clips_backlinks
Test: atest AppClipsActivityTest AppClipsViewModelTest
Change-Id: Ibb88ed83ba3439e175a6749389b497ea8735a823
parent 33dbef15
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:drawablePadding="4dp"
    android:ellipsize="end"
    android:gravity="center_vertical"
    android:paddingHorizontal="8dp"
    android:textColor="?android:textColorSecondary" />
+76 −7
Original line number Diff line number Diff line
@@ -46,13 +46,17 @@ import android.os.Bundle;
import android.os.ResultReceiver;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ListPopupWindow;
import android.widget.TextView;

import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
@@ -67,6 +71,7 @@ import com.android.systemui.res.R;
import com.android.systemui.screenshot.scroll.CropView;
import com.android.systemui.settings.UserTracker;

import java.util.List;
import java.util.Set;

import javax.inject.Inject;
@@ -92,6 +97,7 @@ public class AppClipsActivity extends ComponentActivity {

    private static final String TAG = AppClipsActivity.class.getSimpleName();
    private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);
    private static final int DRAWABLE_END = 2;

    private final AppClipsViewModel.Factory mViewModelFactory;
    private final PackageManager mPackageManager;
@@ -192,6 +198,7 @@ public class AppClipsActivity extends ComponentActivity {
        mViewModel.getResultLiveData().observe(this, this::setResultThenFinish);
        mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish);
        mViewModel.getBacklinksLiveData().observe(this, this::setBacklinksData);
        mViewModel.mSelectedBacklinksLiveData.observe(this, this::updateBacklinksTextView);

        if (savedInstanceState == null) {
            int displayId = getDisplayId();
@@ -305,8 +312,8 @@ public class AppClipsActivity extends ComponentActivity {

        if (mBacklinksIncludeDataCheckBox.getVisibility() == View.VISIBLE
                && mBacklinksIncludeDataCheckBox.isChecked()
                && mViewModel.getBacklinksLiveData().getValue() != null) {
            ClipData backlinksData = mViewModel.getBacklinksLiveData().getValue().getClipData();
                && mViewModel.mSelectedBacklinksLiveData.getValue() != null) {
            ClipData backlinksData = mViewModel.mSelectedBacklinksLiveData.getValue().getClipData();
            data.putParcelable(EXTRA_CLIP_DATA, backlinksData);

            DebugLogger.INSTANCE.logcatMessage(this,
@@ -330,18 +337,80 @@ public class AppClipsActivity extends ComponentActivity {
        finish();
    }

    private void setBacklinksData(InternalBacklinksData backlinksData) {
    private void setBacklinksData(List<InternalBacklinksData> backlinksData) {
        mBacklinksIncludeDataCheckBox.setVisibility(View.VISIBLE);
        mBacklinksDataTextView.setVisibility(
                mBacklinksIncludeDataCheckBox.isChecked() ? View.VISIBLE : View.GONE);

        mBacklinksDataTextView.setText(backlinksData.getClipData().getDescription().getLabel());
        // Set up the dropdown when multiple backlinks are available.
        if (backlinksData.size() > 1) {
            setUpListPopupWindow(backlinksData, mBacklinksDataTextView);
        }
    }

    private void setUpListPopupWindow(List<InternalBacklinksData> backlinksData, View anchor) {
        ListPopupWindow listPopupWindow = new ListPopupWindow(this);
        listPopupWindow.setAnchorView(anchor);
        listPopupWindow.setOverlapAnchor(true);
        listPopupWindow.setBackgroundDrawable(
                AppCompatResources.getDrawable(this, R.drawable.backlinks_rounded_rectangle));
        listPopupWindow.setOnItemClickListener((parent, view, position, id) -> {
            mViewModel.mSelectedBacklinksLiveData.setValue(backlinksData.get(position));
            listPopupWindow.dismiss();
        });

        ArrayAdapter<InternalBacklinksData> adapter = new ArrayAdapter<>(this,
                R.layout.app_clips_backlinks_drop_down_entry) {
            @Override
            public View getView(int position, @Nullable View convertView, ViewGroup parent) {
                TextView itemView = (TextView) super.getView(position, convertView, parent);
                InternalBacklinksData data = backlinksData.get(position);
                itemView.setText(data.getClipData().getDescription().getLabel());

                Drawable icon = data.getAppIcon();
                icon.setBounds(createBacklinksTextViewDrawableBounds());
                itemView.setCompoundDrawablesRelative(/* start= */ icon, /* top= */ null,
                        /* end= */ null, /* bottom= */ null);

                return itemView;
            }
        };
        adapter.addAll(backlinksData);
        listPopupWindow.setAdapter(adapter);

        mBacklinksDataTextView.setOnClickListener(unused -> listPopupWindow.show());
    }

    /**
     * Updates the {@link #mBacklinksDataTextView} with the currently selected
     * {@link InternalBacklinksData}. The {@link AppClipsViewModel#getBacklinksLiveData()} is
     * expected to be already set when this method is called.
     */
    private void updateBacklinksTextView(InternalBacklinksData backlinksData) {
        mBacklinksDataTextView.setText(backlinksData.getClipData().getDescription().getLabel());
        Drawable appIcon = backlinksData.getAppIcon();
        int size = getResources().getDimensionPixelSize(R.dimen.appclips_backlinks_icon_size);
        appIcon.setBounds(/* left= */ 0, /* top= */ 0, /* right= */ size, /* bottom= */ size);
        Rect compoundDrawableBounds = createBacklinksTextViewDrawableBounds();
        appIcon.setBounds(compoundDrawableBounds);

        // Try to reuse the dropdown down arrow icon if available, will be null if never set.
        Drawable dropDownIcon = mBacklinksDataTextView.getCompoundDrawablesRelative()[DRAWABLE_END];
        if (mViewModel.getBacklinksLiveData().getValue().size() > 1 && dropDownIcon == null) {
            // Set up the dropdown down arrow drawable only if it is required.
            dropDownIcon = AppCompatResources.getDrawable(this, R.drawable.arrow_pointing_down);
            dropDownIcon.setBounds(compoundDrawableBounds);
            dropDownIcon.setTint(Utils.getColorAttr(this,
                    android.R.attr.textColorSecondary).getDefaultColor());
        }

        mBacklinksDataTextView.setCompoundDrawablesRelative(/* start= */ appIcon, /* top= */
                null, /* end= */ null, /* bottom= */ null);
                null, /* end= */ dropDownIcon, /* bottom= */ null);
    }

    private Rect createBacklinksTextViewDrawableBounds() {
        int size = getResources().getDimensionPixelSize(R.dimen.appclips_backlinks_icon_size);
        Rect bounds = new Rect();
        bounds.set(/* left= */ 0, /* top= */ 0, /* right= */ size, /* bottom= */ size);
        return bounds;
    }

    private void setError(int errorCode) {
+97 −48
Original line number Diff line number Diff line
@@ -21,12 +21,11 @@ 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.TaskInfo;
import android.app.WindowConfiguration;
import android.app.assist.AssistContent;
import android.content.ClipData;
@@ -41,7 +40,6 @@ 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;
@@ -94,7 +92,8 @@ final class AppClipsViewModel extends ViewModel {
    private final MutableLiveData<Bitmap> mScreenshotLiveData;
    private final MutableLiveData<Uri> mResultLiveData;
    private final MutableLiveData<Integer> mErrorLiveData;
    private final MutableLiveData<InternalBacklinksData> mBacklinksLiveData;
    private final MutableLiveData<List<InternalBacklinksData>> mBacklinksLiveData;
    final MutableLiveData<InternalBacklinksData> mSelectedBacklinksLiveData;

    private AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper,
            ImageExporter imageExporter, IActivityTaskManager atmService,
@@ -112,6 +111,7 @@ final class AppClipsViewModel extends ViewModel {
        mResultLiveData = new MutableLiveData<>();
        mErrorLiveData = new MutableLiveData<>();
        mBacklinksLiveData = new MutableLiveData<>();
        mSelectedBacklinksLiveData = new MutableLiveData<>();
    }

    /**
@@ -135,10 +135,11 @@ 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()}.
     *     <li>Evaluates the tasks to query.
     *     <li>Requests {@link AssistContent} from all valid tasks.
     *     <li>Transforms {@link AssistContent} into {@link InternalBacklinksData} for Backlinks.
     *     <li>The {@link InternalBacklinksData}s are reported to activity via
     *     {@link #getBacklinksLiveData()}.
     * </ul>
     *
     * @param taskIdsToIgnore id of the tasks to ignore when querying for {@link AssistContent}
@@ -146,14 +147,16 @@ final class AppClipsViewModel extends ViewModel {
     */
    void triggerBacklinks(Set<Integer> taskIdsToIgnore, int displayId) {
        DebugLogger.INSTANCE.logcatMessage(this, () -> "Backlinks triggered");
        mBgExecutor.execute(() -> {
            ListenableFuture<InternalBacklinksData> backlinksData = getBacklinksData(
        ListenableFuture<List<InternalBacklinksData>> backlinksData = getAllAvailableBacklinks(
                taskIdsToIgnore, displayId);
        Futures.addCallback(backlinksData, new FutureCallback<>() {
            @Override
                public void onSuccess(@Nullable InternalBacklinksData result) {
                    if (result != null) {
            public void onSuccess(@Nullable List<InternalBacklinksData> result) {
                if (result != null && !result.isEmpty()) {
                    // Set the list of backlinks before setting the selected backlink as this is
                    // required when updating the backlink data text view.
                    mBacklinksLiveData.setValue(result);
                    mSelectedBacklinksLiveData.setValue(result.get(0));
                }
            }

@@ -162,8 +165,6 @@ final class AppClipsViewModel extends ViewModel {
                Log.e(TAG, "Error querying for Backlinks data", t);
            }
        }, mMainExecutor);

        });
    }

    /** Returns a {@link LiveData} that holds the captured screenshot. */
@@ -184,8 +185,11 @@ final class AppClipsViewModel extends ViewModel {
        return mErrorLiveData;
    }

    /** Returns a {@link LiveData} that holds Backlinks data in {@link InternalBacklinksData}. */
    LiveData<InternalBacklinksData> getBacklinksLiveData() {
    /**
     * Returns a {@link LiveData} that holds all the available Backlinks data and the currently
     * selected index for displaying the Backlinks in the UI.
     */
    LiveData<List<InternalBacklinksData>> getBacklinksLiveData() {
        return mBacklinksLiveData;
    }

@@ -230,26 +234,58 @@ final class AppClipsViewModel extends ViewModel {
        return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
    }

    private ListenableFuture<InternalBacklinksData> getBacklinksData(Set<Integer> taskIdsToIgnore,
            int displayId) {
        return getAllRootTaskInfosOnDisplay(displayId)
    private ListenableFuture<List<InternalBacklinksData>> getAllAvailableBacklinks(
            Set<Integer> taskIdsToIgnore, int displayId) {
        ListenableFuture<List<TaskInfo>> allTasksOnDisplayFuture = getAllTasksOnDisplay(displayId);

        ListenableFuture<List<ListenableFuture<InternalBacklinksData>>> backlinksNestedListFuture =
                Futures.transform(allTasksOnDisplayFuture, allTasksOnDisplay ->
                        allTasksOnDisplay
                                .stream()
                                .filter(taskInfo -> shouldIncludeTask(taskInfo, taskIdsToIgnore))
                .findFirst()
                .map(this::getBacklinksDataForTaskId)
                .orElse(Futures.immediateFuture(null));
                                .map(this::getBacklinksDataForTaskInfo)
                                .toList(),
                        mBgExecutor);

        return Futures.transformAsync(backlinksNestedListFuture, Futures::allAsList, mBgExecutor);
    }

    private List<RootTaskInfo> getAllRootTaskInfosOnDisplay(int displayId) {
    /**
     * Returns all tasks on a given display after querying {@link IActivityTaskManager} from the
     * {@link #mBgExecutor}.
     */
    private ListenableFuture<List<TaskInfo>> getAllTasksOnDisplay(int displayId) {
        SettableFuture<List<TaskInfo>> recentTasksFuture = SettableFuture.create();
        mBgExecutor.execute(() -> {
            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();
                // Directly call into ActivityTaskManagerService instead of going through WMShell
                // because WMShell is only available in the main SysUI process and App Clips runs
                // in its own separate process as it deals with bitmaps.
                List<TaskInfo> allTasksOnDisplay = mAtmService.getTasks(
                                /* maxNum= */ Integer.MAX_VALUE,
                                // PIP tasks are not visible in recents. So _not_ filtering for
                                // tasks that are only visible in recents.
                                /* filterOnlyVisibleRecents= */ false,
                                /* keepIntentExtra= */ false,
                                displayId)
                        .stream()
                        .map(runningTaskInfo -> (TaskInfo) runningTaskInfo)
                        .toList();
                recentTasksFuture.set(allTasksOnDisplay);
            } catch (Exception e) {
                Log.e(TAG, String.format("Error getting all tasks on displayId %d", displayId), e);
                recentTasksFuture.set(Collections.emptyList());
            }
        });

        return withTimeout(recentTasksFuture);
    }

    private boolean shouldIncludeTask(RootTaskInfo taskInfo, Set<Integer> taskIdsToIgnore) {
    /**
     * Returns whether the app represented by the provided {@link TaskInfo} should be included for
     * querying for {@link AssistContent}.
     */
    private boolean shouldIncludeTask(TaskInfo taskInfo, Set<Integer> taskIdsToIgnore) {
        DebugLogger.INSTANCE.logcatMessage(this,
                () -> String.format("shouldIncludeTask taskId %d; topActivity %s", taskInfo.taskId,
                        taskInfo.topActivity));
@@ -262,11 +298,14 @@ final class AppClipsViewModel extends ViewModel {
                && taskInfo.numActivities > 0
                && taskInfo.topActivity != null
                && taskInfo.topActivityInfo != null
                && taskInfo.childTaskIds.length > 0
                && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD
                && canAppStartThroughLauncher(taskInfo.topActivity.getPackageName());
    }

    /**
     * Returns whether the app represented by the provided {@code packageName} can be launched
     * through the all apps tray by a user.
     */
    private boolean canAppStartThroughLauncher(String packageName) {
        // Use Intent.resolveActivity API to check if the intent resolves as that is what Android
        // uses internally when apps use Context.startActivity.
@@ -274,8 +313,12 @@ final class AppClipsViewModel extends ViewModel {
                != null;
    }

    private ListenableFuture<InternalBacklinksData> getBacklinksDataForTaskId(
            RootTaskInfo taskInfo) {
    /**
     * Returns an {@link InternalBacklinksData} that represents the Backlink data internally, which
     * is captured by querying the system using {@link TaskInfo#taskId}.
     */
    private ListenableFuture<InternalBacklinksData> getBacklinksDataForTaskInfo(
            TaskInfo taskInfo) {
        DebugLogger.INSTANCE.logcatMessage(this,
                () -> String.format("getBacklinksDataForTaskId for taskId %d; topActivity %s",
                        taskInfo.taskId, taskInfo.topActivity));
@@ -284,7 +327,13 @@ final class AppClipsViewModel extends ViewModel {
        int taskId = taskInfo.taskId;
        mAssistContentRequester.requestAssistContent(taskId, assistContent ->
                backlinksData.set(getBacklinksDataFromAssistContent(taskInfo, assistContent)));
        return withTimeout(backlinksData, 5L, TimeUnit.SECONDS, newSingleThreadScheduledExecutor());
        return withTimeout(backlinksData);
    }

    /** Returns the same {@link ListenableFuture} but with a 5 {@link TimeUnit#SECONDS} timeout. */
    private static <V> ListenableFuture<V> withTimeout(ListenableFuture<V> future) {
        return Futures.withTimeout(future, 5L, TimeUnit.SECONDS,
                newSingleThreadScheduledExecutor());
    }

    /**
@@ -306,7 +355,7 @@ final class AppClipsViewModel extends ViewModel {
     * @param content the {@link AssistContent} to map into Backlinks {@link ClipData}.
     * @return {@link InternalBacklinksData} that represents the Backlinks data along with app icon.
     */
    private InternalBacklinksData getBacklinksDataFromAssistContent(RootTaskInfo taskInfo,
    private InternalBacklinksData getBacklinksDataFromAssistContent(TaskInfo taskInfo,
            @Nullable AssistContent content) {
        DebugLogger.INSTANCE.logcatMessage(this,
                () -> String.format("getBacklinksDataFromAssistContent taskId %d; topActivity %s",
@@ -365,7 +414,7 @@ final class AppClipsViewModel extends ViewModel {
        return resolvedComponent.getPackageName().equals(requiredPackageName);
    }

    private String getAppNameOfTask(RootTaskInfo taskInfo) {
    private String getAppNameOfTask(TaskInfo taskInfo) {
        return taskInfo.topActivityInfo.loadLabel(mPackageManager).toString();
    }

+65 −11
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.screenshot.appclips;

import static android.app.Activity.RESULT_OK;
import static android.app.ActivityManager.RunningTaskInfo;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;

import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_ACCEPTED;
@@ -32,7 +33,6 @@ 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;
@@ -103,7 +103,7 @@ public final class AppClipsActivityTest extends SysuiTestCase {
    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 =
    private static final RunningTaskInfo TASK_THAT_SUPPORTS_BACKLINKS =
            createTaskInfoForBacklinksTask();
    private static final AssistContent ASSIST_CONTENT_FOR_BACKLINKS_TASK =
            createAssistContentForBacklinksTask();
@@ -233,6 +233,10 @@ public final class AppClipsActivityTest extends SysuiTestCase {
        assertThat(backlinksData.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME);
        assertThat(backlinksData.getCompoundDrawablesRelative()[0]).isEqualTo(FAKE_DRAWABLE);

        // Verify dropdown icon is not shown and there are no click listeners on text view.
        assertThat(backlinksData.getCompoundDrawablesRelative()[2]).isNull();
        assertThat(backlinksData.hasOnClickListeners()).isFalse();

        CheckBox backlinksIncludeData = mActivity.findViewById(R.id.backlinks_include_data);
        assertThat(backlinksIncludeData.getVisibility()).isEqualTo(View.VISIBLE);
        assertThat(backlinksIncludeData.getText().toString())
@@ -258,20 +262,71 @@ public final class AppClipsActivityTest extends SysuiTestCase {
        assertThat(backlinksData.getVisibility()).isEqualTo(View.GONE);
    }

    @Test
    @EnableFlags(Flags.FLAG_APP_CLIPS_BACKLINKS)
    public void appClipsLaunched_backlinks_multipleBacklinksAvailable_defaultShown()
            throws RemoteException {
        // Set up mocking for multiple backlinks.
        ResolveInfo resolveInfo1 = createBacklinksTaskResolveInfo();

        int taskId2 = BACKLINKS_TASK_ID + 2;
        String package2 = BACKLINKS_TASK_PACKAGE_NAME + 2;
        String appName2 = BACKLINKS_TASK_APP_NAME + 2;

        ResolveInfo resolveInfo2 = createBacklinksTaskResolveInfo();
        ActivityInfo activityInfo2 = resolveInfo2.activityInfo;
        activityInfo2.name = appName2;
        activityInfo2.packageName = package2;
        activityInfo2.applicationInfo.packageName = package2;
        RunningTaskInfo runningTaskInfo2 = createTaskInfoForBacklinksTask();
        runningTaskInfo2.taskId = taskId2;
        runningTaskInfo2.topActivity = new ComponentName(package2, "backlinksClass");
        runningTaskInfo2.topActivityInfo = resolveInfo2.activityInfo;
        runningTaskInfo2.baseIntent = new Intent().setComponent(runningTaskInfo2.topActivity);

        when(mAtmService.getTasks(eq(Integer.MAX_VALUE), eq(false), eq(false),
                mDisplayIdCaptor.capture()))
                .thenReturn(List.of(TASK_THAT_SUPPORTS_BACKLINKS, runningTaskInfo2));
        when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn(resolveInfo1,
                resolveInfo1, resolveInfo1, resolveInfo2, resolveInfo2, resolveInfo2);
        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);

        // Using same AssistContent data for both tasks.
        mockForAssistContent(ASSIST_CONTENT_FOR_BACKLINKS_TASK, BACKLINKS_TASK_ID);
        mockForAssistContent(ASSIST_CONTENT_FOR_BACKLINKS_TASK, taskId2);

        // Mocking complete, trigger backlinks.
        launchActivity();
        waitForIdleSync();

        // Verify default backlink shown to user and text view has on click listener.
        TextView backlinksData = mActivity.findViewById(R.id.backlinks_data);
        assertThat(backlinksData.getText().toString()).isEqualTo(BACKLINKS_TASK_APP_NAME);
        assertThat(backlinksData.hasOnClickListeners()).isTrue();

        // Verify dropdown icon is not null.
        assertThat(backlinksData.getCompoundDrawablesRelative()[2]).isNotNull();
    }

    private void setUpMocksForBacklinks() throws RemoteException {
        when(mAtmService.getAllRootTaskInfosOnDisplay(mDisplayIdCaptor.capture()))
        when(mAtmService.getTasks(eq(Integer.MAX_VALUE), eq(false), eq(false),
                mDisplayIdCaptor.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());
        mockForAssistContent(ASSIST_CONTENT_FOR_BACKLINKS_TASK, BACKLINKS_TASK_ID);
        when(mPackageManager
                .resolveActivity(any(Intent.class), anyInt()))
                .thenReturn(createBacklinksTaskResolveInfo());
        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);
    }

    private void mockForAssistContent(AssistContent expected, int taskId) {
        doAnswer(invocation -> {
            AssistContentRequester.Callback callback = invocation.getArgument(1);
            callback.onAssistContentAvailable(expected);
            return null;
        }).when(mAssistContentRequester).requestAssistContent(eq(taskId), any());
    }

    private void launchActivity() {
        launchActivity(createResultReceiver(FAKE_CONSUMER));
    }
@@ -319,8 +374,8 @@ public final class AppClipsActivityTest extends SysuiTestCase {
        return resolveInfo;
    }

    private static RootTaskInfo createTaskInfoForBacklinksTask() {
        RootTaskInfo taskInfo = new RootTaskInfo();
    private static RunningTaskInfo createTaskInfoForBacklinksTask() {
        RunningTaskInfo taskInfo = new RunningTaskInfo();
        taskInfo.taskId = BACKLINKS_TASK_ID;
        taskInfo.isVisible = true;
        taskInfo.isRunning = true;
@@ -328,7 +383,6 @@ public final class AppClipsActivityTest extends SysuiTestCase {
        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;
    }
+86 −40

File changed.

Preview size limit exceeded, changes collapsed.