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

Commit d4a8bd48 authored by Ajinkya Chalke's avatar Ajinkya Chalke Committed by Android (Google) Code Review
Browse files

Merge "Update Backlinks to capture data from all tasks" into main

parents 8d577e62 4297e828
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.