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

Commit 5ea8fe5c authored by Joe Antonetti's avatar Joe Antonetti
Browse files

[Handoff][10/N] Filter Launcher Tasks from Handoff

This change removes launcher tasks from Handoff sync.

Flag: android.companion.enable_task_continuity
Bug: 400970610
Test: Updated Unit Tests
Change-Id: I820c737c57a9d8d77357adc0ad9f0b4674ba7f3f
parent 2a70c013
Loading
Loading
Loading
Loading
+25 −76
Original line number Diff line number Diff line
@@ -31,13 +31,10 @@ import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskA
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskRemovedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskUpdatedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadata;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadataCache;
import com.android.server.companion.datatransfer.continuity.tasks.RunningTaskFetcher;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Responsible for broadcasting recent tasks on the current device to the user's
@@ -48,29 +45,37 @@ class TaskBroadcaster extends TaskStackListener {

    private static final String TAG = "TaskBroadcaster";

    private final Context mContext;
    private final ActivityTaskManager mActivityTaskManager;
    private final TaskContinuityMessenger mTaskContinuityMessenger;
    private final PackageMetadataCache mPackageMetadataCache;
    private final RunningTaskFetcher mRunningTaskFetcher;

    private boolean mIsListeningToActivityTaskManager = false;

    public TaskBroadcaster(
        @NonNull Context context,
        @NonNull TaskContinuityMessenger taskContinuityMessenger) {
        this(
            Objects.requireNonNull(taskContinuityMessenger),
            Objects.requireNonNull(context).getSystemService(ActivityTaskManager.class),
            new RunningTaskFetcher(Objects.requireNonNull(context)));
    }

        Objects.requireNonNull(context);
        Objects.requireNonNull(taskContinuityMessenger);
    public TaskBroadcaster(
        @NonNull TaskContinuityMessenger taskContinuityMessenger,
        @NonNull ActivityTaskManager activityTaskManager,
        @NonNull RunningTaskFetcher runningTaskFetcher) {

        mContext = context;
        mActivityTaskManager = context.getSystemService(ActivityTaskManager.class);
        mPackageMetadataCache = new PackageMetadataCache(context.getPackageManager());
        mTaskContinuityMessenger = taskContinuityMessenger;
        mTaskContinuityMessenger = Objects.requireNonNull(taskContinuityMessenger);
        mActivityTaskManager = Objects.requireNonNull(activityTaskManager);
        mRunningTaskFetcher = Objects.requireNonNull(runningTaskFetcher);
    }

    public void onDeviceConnected(int id) {
        Slog.v(TAG, "Transport connected for association id: " + id);
        sendDeviceConnectedMessage(id);
    public void onDeviceConnected(int associationId) {
        Slog.v(TAG, "Transport connected for association id: " + associationId);
        mTaskContinuityMessenger.sendMessage(
            associationId,
            new ContinuityDeviceConnected(mRunningTaskFetcher.getRunningTasks()));

        synchronized (this) {
            if (!mIsListeningToActivityTaskManager) {
                mActivityTaskManager.registerTaskStackListener(this);
@@ -91,34 +96,27 @@ class TaskBroadcaster extends TaskStackListener {
    @Override
    public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException {
        Slog.v(TAG, "onTaskCreated: taskId=" + taskId);
        RunningTaskInfo taskInfo = getRunningTask(taskId);
        if (taskInfo == null) {
            Slog.w(TAG, "Could not find RunningTaskInfo for taskId: " + taskId);
            return;
        }

        RemoteTaskInfo remoteTaskInfo = createRemoteTaskInfo(taskInfo);
        RemoteTaskInfo remoteTaskInfo = mRunningTaskFetcher.getRunningTaskById(taskId);
        if (remoteTaskInfo == null) {
            Slog.w(TAG, "Could not create RemoteTaskInfo for task: " + taskInfo.taskId);
            Slog.w(TAG, "Could not create RemoteTaskInfo for task: " + taskId);
            return;
        }

        RemoteTaskAddedMessage taskAddedMessage = new RemoteTaskAddedMessage(remoteTaskInfo);
        mTaskContinuityMessenger.sendMessage(taskAddedMessage);
        mTaskContinuityMessenger.sendMessage(new RemoteTaskAddedMessage(remoteTaskInfo));
    }

    @Override
    public void onTaskRemoved(int taskId) throws RemoteException {
        Slog.v(TAG, "onTaskRemoved: taskId=" + taskId);
        RemoteTaskRemovedMessage taskRemovedMessage = new RemoteTaskRemovedMessage(taskId);
        mTaskContinuityMessenger.sendMessage(taskRemovedMessage);
        mTaskContinuityMessenger.sendMessage(new RemoteTaskRemovedMessage(taskId));
    }

    @Override
    public void onTaskMovedToFront(RunningTaskInfo taskInfo) throws RemoteException {
        Slog.v(TAG, "onTaskMovedToFront: taskId=" + taskInfo.taskId);

        RemoteTaskInfo remoteTaskInfo = createRemoteTaskInfo(taskInfo);
        RemoteTaskInfo remoteTaskInfo = mRunningTaskFetcher.getRunningTaskById(taskInfo.taskId);
        if (remoteTaskInfo == null) {
            Slog.w(TAG, "Could not create RemoteTaskInfo for task: " + taskInfo.taskId);
            return;
@@ -127,53 +125,4 @@ class TaskBroadcaster extends TaskStackListener {
        RemoteTaskUpdatedMessage taskUpdatedMessage = new RemoteTaskUpdatedMessage(remoteTaskInfo);
        mTaskContinuityMessenger.sendMessage(taskUpdatedMessage);
    }

    private void sendDeviceConnectedMessage(int associationId) {
        Slog.v(
            TAG,
            "Sending device connected message for association id: "
                + associationId);

        List<RemoteTaskInfo> remoteTasks = getRunningTasks().stream()
            .map(this::createRemoteTaskInfo)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

        ContinuityDeviceConnected deviceConnectedMessage
            = new ContinuityDeviceConnected(remoteTasks);

        mTaskContinuityMessenger.sendMessage(associationId, deviceConnectedMessage);
    }

    private RunningTaskInfo getRunningTask(int taskId) {
        List<RunningTaskInfo> runningTasks = getRunningTasks();
        if (runningTasks != null) {
            for (RunningTaskInfo info : runningTasks) {
                if (info.taskId == taskId) {
                    return info;
                }
            }
        }

        return null;
    }

    private List<RunningTaskInfo> getRunningTasks() {
        return mActivityTaskManager.getTasks(Integer.MAX_VALUE, true);
    }

    private RemoteTaskInfo createRemoteTaskInfo(RunningTaskInfo taskInfo) {
        PackageMetadata packageMetadata = mPackageMetadataCache.getMetadataForPackage(
            taskInfo.baseActivity.getPackageName());
        if (packageMetadata == null) {
            Slog.w(TAG, "Could not get package metadata for task: " + taskInfo.taskId);
            return null;
        }

        return new RemoteTaskInfo(
            taskInfo.taskId,
            packageMetadata.label(),
            taskInfo.lastActiveTime,
            packageMetadata.icon());
    }
}
 No newline at end of file
+146 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.
 */

package com.android.server.companion.datatransfer.continuity.tasks;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.pm.PackageManager;
import android.content.Context;
import android.content.Intent;
import android.util.Slog;

import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadata;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadataCache;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * Fetches the running tasks from the system, converts them to {@link RemoteTaskInfo} and filters
 * out the tasks that should not be synced.
 */
public class RunningTaskFetcher {

    private static final String TAG = "RunningTaskFetcher";

    private final ActivityTaskManager mActivityTaskManager;
    private final PackageManager mPackageManager;
    private final PackageMetadataCache mPackageMetadataCache;

    public RunningTaskFetcher(@NonNull Context context) {
        this(
            Objects.requireNonNull(context).getSystemService(ActivityTaskManager.class),
            Objects.requireNonNull(context).getPackageManager(),
            new PackageMetadataCache(Objects.requireNonNull(context).getPackageManager()));
    }

    public RunningTaskFetcher(
        @NonNull ActivityTaskManager activityTaskManager,
        @NonNull PackageManager packageManager,
        @NonNull PackageMetadataCache packageMetadataCache) {

        mActivityTaskManager = Objects.requireNonNull(activityTaskManager);
        mPackageManager = Objects.requireNonNull(packageManager);
        mPackageMetadataCache = Objects.requireNonNull(packageMetadataCache);
    }

    @Nullable
    public RemoteTaskInfo getRunningTaskById(int taskId) {
        RemoteTaskInfo taskInfo = getRunningTasks()
            .stream()
            .filter(info -> info.id() == taskId)
            .findFirst()
            .orElse(null);

        if (taskInfo == null) {
            Slog.w(TAG, "Could not find RunningTaskInfo for taskId: " + taskId);
            return null;
        }

        return taskInfo;
    }

    @NonNull
    public List<RemoteTaskInfo> getRunningTasks() {
        return getRunningTaskInfos()
            .stream()
            .filter(this::shouldTaskBeSynced)
            .map(this::createRemoteTaskInfo)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }

    @Nullable
    private RemoteTaskInfo createRemoteTaskInfo(@Nullable RunningTaskInfo taskInfo) {
        if (taskInfo == null) {
            return null;
        }

        if (taskInfo.baseActivity == null || taskInfo.baseActivity.getPackageName() == null) {
            Slog.w(TAG, "Package name is null for task: " + taskInfo.taskId);
            return null;
        }

        String packageName = taskInfo.baseActivity.getPackageName();
        PackageMetadata packageMetadata = mPackageMetadataCache.getMetadataForPackage(packageName);
        if (packageMetadata == null) {
            Slog.w(TAG, "Could not get package metadata for task: " + taskInfo.taskId);
            return null;
        }

        return new RemoteTaskInfo(
            taskInfo.taskId,
            packageMetadata.label(),
            taskInfo.lastActiveTime,
            packageMetadata.icon());
    }

    @NonNull
    private List<RunningTaskInfo> getRunningTaskInfos() {
        List<RunningTaskInfo> runningTaskInfos
            = mActivityTaskManager.getTasks(Integer.MAX_VALUE, true);
        if (runningTaskInfos == null) {
            return new ArrayList<>();
        }

        return runningTaskInfos;
    }

    private boolean shouldTaskBeSynced(@Nullable RunningTaskInfo taskInfo) {
        if (taskInfo == null) {
            return false;
        }

        Intent intent = new Intent("android.intent.action.MAIN");
        intent.addCategory("android.intent.category.HOME");
        String defaultLauncherPackage = mPackageManager
            .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
            .activityInfo
            .packageName;
        if (defaultLauncherPackage == null) {
            Slog.w(TAG, "Could not get default launcher package");
            return true;
        }

        return !defaultLauncherPackage.equals(taskInfo.baseActivity.getPackageName());
    }
}
 No newline at end of file
+58 −121
Original line number Diff line number Diff line
@@ -23,28 +23,14 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.never;
import static org.mockito.ArgumentMatchers.eq;

import static com.android.server.companion.datatransfer.contextsync.BitmapUtils.renderDrawableToByteArray;

import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.companion.datatransfer.continuity.RemoteTask;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.server.companion.datatransfer.continuity.connectivity.TaskContinuityMessenger;

import com.android.server.companion.datatransfer.continuity.messages.ContinuityDeviceConnected;
@@ -52,61 +38,34 @@ import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskA
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskRemovedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskUpdatedMessage;

import com.android.frameworks.servicestests.R;
import com.android.server.companion.datatransfer.continuity.tasks.RunningTaskFetcher;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Presubmit
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class TaskBroadcasterTest {

    private Context mMockContext;

    @Mock private ActivityTaskManager mMockActivityTaskManager;
    @Mock private RunningTaskFetcher mMockRunningTaskFetcher;
    @Mock private TaskContinuityMessenger mMockTaskContinuityMessenger;
    @Mock private PackageManager mMockPackageManager;

    private TaskBroadcaster mTaskBroadcaster;

    private Drawable mTaskIcon;
    private byte[] mSerializedTaskIcon;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mMockContext = Mockito.spy(
            new ContextWrapper(
                InstrumentationRegistry
                    .getInstrumentation()
                    .getTargetContext()));

        when(mMockContext.getSystemService(Context.ACTIVITY_TASK_SERVICE))
            .thenReturn(mMockActivityTaskManager);

        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);

        Bitmap bitmap = BitmapFactory.decodeResource(
                mMockContext.getResources(), R.drawable.black_32x32);
        mTaskIcon = new BitmapDrawable(mMockContext.getResources(), bitmap);
        mSerializedTaskIcon = renderDrawableToByteArray(mTaskIcon);

        // Create TaskBroadcaster.
        mTaskBroadcaster = new TaskBroadcaster(
            mMockContext,
            mMockTaskContinuityMessenger);
            mMockTaskContinuityMessenger,
            mMockActivityTaskManager,
            mMockRunningTaskFetcher);
    }

    @Test
@@ -127,29 +86,21 @@ public class TaskBroadcasterTest {
    }

    @Test
    public void testOnDeviceConnected_sendsMessageToDevice()
        throws RemoteException, NameNotFoundException {

        // Setup a fake foreground task.
        int taskId = 100;
        String taskLabel = "test";
        long taskLastActiveTime = 100;
        RunningTaskInfo taskInfo = setupTask(taskId, taskLabel, taskLastActiveTime);

        when(mMockActivityTaskManager.getTasks(Integer.MAX_VALUE, true))
            .thenReturn(Arrays.asList(taskInfo));
    public void testOnDeviceConnected_sendsMessageToDevice() throws RemoteException {
        RemoteTaskInfo expectedRemoteTaskInfo = new RemoteTaskInfo(
            100 /* taskId */,
            "test" /* label */,
            100 /* lastActiveTime */,
            new byte[0] /* icon */);
        when(mMockRunningTaskFetcher.getRunningTasks())
            .thenReturn(List.of(expectedRemoteTaskInfo));

        // Add a new transport
        int associationId = 1;
        mTaskBroadcaster.onDeviceConnected(associationId);

        // Verify the message is sent.
        ContinuityDeviceConnected expectedMessage = new ContinuityDeviceConnected(
            Arrays.asList(new RemoteTaskInfo(
                taskId,
                taskLabel,
                taskLastActiveTime,
                mSerializedTaskIcon)));
        ContinuityDeviceConnected expectedMessage
            = new ContinuityDeviceConnected(List.of(expectedRemoteTaskInfo));
        verify(mMockTaskContinuityMessenger, times(1)).sendMessage(
            eq(associationId),
            eq(expectedMessage));
@@ -159,32 +110,34 @@ public class TaskBroadcasterTest {
    }

    @Test
    public void testOnTaskCreated_sendsMessageToAllAssociations()
        throws NameNotFoundException, RemoteException {

        // Define a new task.
        String taskLabel = "newTask";
        int taskId = 123;
        long taskLastActiveTime = 0;
        RunningTaskInfo taskInfo = setupTask(taskId, taskLabel, taskLastActiveTime);

        // Mock ActivityTaskManager to return the new task.
        when(mMockActivityTaskManager.getTasks(Integer.MAX_VALUE, true))
                .thenReturn(List.of(taskInfo));
    public void testOnTaskCreated_sendsMessageToAllAssociations() throws RemoteException {
        RemoteTaskInfo expectedRemoteTaskInfo = new RemoteTaskInfo(
            123 /* taskId */,
            "newTask" /* label */,
            0 /* lastActiveTime */,
            new byte[0] /* icon */);
        when(mMockRunningTaskFetcher.getRunningTaskById(expectedRemoteTaskInfo.id()))
            .thenReturn(expectedRemoteTaskInfo);

        // Notify TaskBroadcaster of the new task.
        mTaskBroadcaster.onTaskCreated(taskId, null);
        mTaskBroadcaster.onTaskCreated(expectedRemoteTaskInfo.id(), null);

        // Verify sendMessage is called
        RemoteTaskAddedMessage expectedMessage = new RemoteTaskAddedMessage(
            new RemoteTaskInfo(
                taskId,
                taskLabel,
                taskLastActiveTime,
                mSerializedTaskIcon));
        RemoteTaskAddedMessage expectedMessage = new RemoteTaskAddedMessage(expectedRemoteTaskInfo);
        verify(mMockTaskContinuityMessenger, times(1)).sendMessage(eq(expectedMessage));
    }

    @Test
    public void testOnTaskCreated_taskNotFound_doesNotSendMessage() throws RemoteException {
        int taskId = 123;
        when(mMockRunningTaskFetcher.getRunningTaskById(taskId))
            .thenReturn(null);

        mTaskBroadcaster.onTaskCreated(taskId, null);

        verify(mMockTaskContinuityMessenger, never()).sendMessage(any());
    }

    @Test
    public void testOnTaskRemoved_sendsMessageToAllAssociations() throws RemoteException {
        int taskId = 123;
@@ -197,50 +150,34 @@ public class TaskBroadcasterTest {
    }

    @Test
    public void testOnTaskMovedToFront_sendsMessageToAllAssociations()
        throws NameNotFoundException, RemoteException {

        // Simulate a task being moved to front.
        int taskId = 1;
        String taskLabel = "newTask";
        long taskLastActiveTime = 0;
        RunningTaskInfo taskInfo = setupTask(taskId, taskLabel, taskLastActiveTime);
    public void testOnTaskMovedToFront_sendsMessageToAllAssociations() throws RemoteException {
        RemoteTaskInfo expectedRemoteTaskInfo = new RemoteTaskInfo(
            1 /* taskId */,
            "task" /* label */,
            100 /* lastActiveTime */,
            new byte[0] /* icon */);
        when(mMockRunningTaskFetcher.getRunningTaskById(expectedRemoteTaskInfo.id()))
            .thenReturn(expectedRemoteTaskInfo);
        RunningTaskInfo taskInfo = new RunningTaskInfo();
        taskInfo.taskId = expectedRemoteTaskInfo.id();

        mTaskBroadcaster.onTaskMovedToFront(taskInfo);

        // Verify sendMessage is called for each association.
        RemoteTaskUpdatedMessage expectedMessage = new RemoteTaskUpdatedMessage(
            new RemoteTaskInfo(
                taskId,
                taskLabel,
                taskLastActiveTime,
                mSerializedTaskIcon));
        RemoteTaskUpdatedMessage expectedMessage
            = new RemoteTaskUpdatedMessage(expectedRemoteTaskInfo);
       verify(mMockTaskContinuityMessenger, times(1)).sendMessage(eq(expectedMessage));
    }

    private RunningTaskInfo setupTask(
        int taskId,
        String label,
        long lastActiveTime) throws NameNotFoundException {
    @Test
    public void testOnTaskMovedToFront_taskNotFound_doesNotSendMessage() throws RemoteException {
        RunningTaskInfo taskInfo = new RunningTaskInfo();
        taskInfo.taskId = 123;
        when(mMockRunningTaskFetcher.getRunningTaskById(taskInfo.taskId))
            .thenReturn(null);

        String packageName = "com.example.app";
        mTaskBroadcaster.onTaskMovedToFront(taskInfo);

        RunningTaskInfo taskInfo = new RunningTaskInfo();
        taskInfo.taskId = taskId;
        taskInfo.baseActivity = new ComponentName(packageName, "className");
        taskInfo.lastActiveTime = lastActiveTime;

        PackageInfo packageInfo = new PackageInfo();
        packageInfo.packageName = packageName;
        packageInfo.applicationInfo = new ApplicationInfo();
        packageInfo.applicationInfo.name = packageName;
        when(mMockPackageManager.getPackageInfo(eq(packageName), eq(PackageManager.GET_META_DATA)))
            .thenReturn(packageInfo);
        when(mMockPackageManager.getApplicationLabel(any(ApplicationInfo.class)))
            .thenReturn(label);

        when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class)))
            .thenReturn(mTaskIcon);

        return taskInfo;
        verify(mMockTaskContinuityMessenger, never()).sendMessage(any());
    }
}
 No newline at end of file
+190 −0

File added.

Preview size limit exceeded, changes collapsed.