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

Commit d1d95253 authored by Joe Antonetti's avatar Joe Antonetti
Browse files

Create TaskBroadcaster

Created TaskBroadcaster to send tasks to other devices via CompanionDeviceManager

Flag: android.companion.enable_task_continuity
Test: Added unit tests
Bug: 400970610

Change-Id: I7b86c48c3926e2f9f8a776136a9e20956255b42e
parent 926e0132
Loading
Loading
Loading
Loading
+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;

import static android.companion.CompanionDeviceManager.MESSAGE_TASK_CONTINUITY;

import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.companion.CompanionDeviceManager;
import android.companion.AssociationInfo;
import android.content.Context;
import android.util.Slog;

import com.android.server.companion.datatransfer.continuity.messages.ContinuityDeviceConnected;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessageData;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Responsible for broadcasting recent tasks on the current device to the user's
 * other devices via {@link CompanionDeviceManager}.
 */
class TaskBroadcaster {

    private static final String TAG = "TaskBroadcaster";

    private final Context mContext;
    private final ActivityTaskManager mActivityTaskManager;
    private final CompanionDeviceManager mCompanionDeviceManager;
    private final Set<Integer> mConnectedAssociationIds = new HashSet<>();

    private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener =
        this::onTransportsChanged;

    private boolean mIsBroadcasting = false;

    public TaskBroadcaster(Context context) {
        mContext = context;

        mActivityTaskManager
            = context.getSystemService(ActivityTaskManager.class);

        mCompanionDeviceManager
            = context.getSystemService(CompanionDeviceManager.class);
    }

    void startBroadcasting(){
        if (mIsBroadcasting) {
            Slog.v(TAG, "TaskBroadcaster is already broadcasting");
            return;
        }

        Slog.v(TAG, "Starting broadcasting");
        mCompanionDeviceManager.addOnTransportsChangedListener(
            mContext.getMainExecutor(),
            mOnTransportsChangedListener
        );
        mIsBroadcasting = true;
    }

    void stopBroadcasting(){
        if (!mIsBroadcasting) {
            Slog.v(TAG, "TaskBroadcaster is not broadcasting");
            return;
        }

        Slog.v(TAG, "Stopping broadcasting");
        mIsBroadcasting = false;
        mCompanionDeviceManager.removeOnTransportsChangedListener(
            mOnTransportsChangedListener
        );
    }

    private void onTransportsChanged(List<AssociationInfo> associationInfos) {
        Set<Integer> removedAssociationIds
            = new HashSet<>(mConnectedAssociationIds);

        for (AssociationInfo associationInfo : associationInfos) {
            if (!mConnectedAssociationIds.contains(associationInfo.getId())) {
                sendDeviceConnectedMessage(associationInfo.getId());
            } else {
                removedAssociationIds.remove(associationInfo.getId());
            }

            mConnectedAssociationIds.add(associationInfo.getId());
        }

        for (Integer removedAssociationId : removedAssociationIds) {
            mConnectedAssociationIds.remove(removedAssociationId);
        }
    }

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

        List<ActivityManager.RunningTaskInfo> runningTasks
            = mActivityTaskManager.getTasks(Integer.MAX_VALUE, true);

        int currentForegroundTaskId = -1;
        if (runningTasks.size() > 0) {
            currentForegroundTaskId = runningTasks.get(0).taskId;
        }

        ContinuityDeviceConnected deviceConnectedMessage =
            new ContinuityDeviceConnected(currentForegroundTaskId);

        sendMessage(associationId, deviceConnectedMessage);
    }

    private void sendMessage(int associationId, TaskContinuityMessageData data) {
        Slog.v(
            TAG,
            "Sending message to association id: "
                + associationId);

        TaskContinuityMessage message = new TaskContinuityMessage.Builder()
                .setData(data)
                .build();

        mCompanionDeviceManager.sendMessage(
            CompanionDeviceManager.MESSAGE_TASK_CONTINUITY,
            message.toBytes(),
            new int[] {associationId});
    }
}
 No newline at end of file
+6 −0
Original line number Diff line number Diff line
@@ -31,14 +31,20 @@ import com.android.server.SystemService;
public final class TaskContinuityManagerService extends SystemService {

    private TaskContinuityManagerServiceImpl mTaskContinuityManagerService;
    private TaskBroadcaster mTaskBroadcaster;
    private TaskReceiver mTaskReceiver;

    public TaskContinuityManagerService(Context context) {
        super(context);
        mTaskBroadcaster = new TaskBroadcaster(context);
        mTaskReceiver = new TaskReceiver(context);
    }

    @Override
    public void onStart() {
        mTaskContinuityManagerService = new TaskContinuityManagerServiceImpl();
        mTaskBroadcaster.startBroadcasting();
        mTaskReceiver.startListening();
        publishBinderService(Context.TASK_CONTINUITY_SERVICE, mTaskContinuityManagerService);
    }

+86 −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;

import static android.companion.CompanionDeviceManager.MESSAGE_TASK_CONTINUITY;

import android.content.Context;
import android.companion.CompanionDeviceManager;
import android.util.Slog;

import java.util.function.BiConsumer;

/**
 * Responsible for receiving task continuity messages from the user's other
 * devices.
 */
class TaskReceiver {

    private static final String TAG = "TaskReceiver";

    private final Context mContext;
    private final CompanionDeviceManager mCompanionDeviceManager;

    private final BiConsumer<Integer, byte[]> mOnMessageReceivedListener
        = this::onMessageReceived;

    private boolean mIsListening = false;

    TaskReceiver(Context context) {
        mContext = context;
        mCompanionDeviceManager = context
            .getSystemService(CompanionDeviceManager.class);
    }

    /**
     * Starts listening for task continuity messages.
     */
    void startListening() {
        if (mIsListening) {
            Slog.v(TAG, "TaskReceiver is already listening");
            return;
        }

        mCompanionDeviceManager.addOnMessageReceivedListener(
            mContext.getMainExecutor(),
            MESSAGE_TASK_CONTINUITY,
            mOnMessageReceivedListener
        );

        mIsListening = true;
    }

    /**
     * Stops listening for task continuity messages.
     */
    void stopListening() {
        if (!mIsListening) {
            Slog.v(TAG, "TaskReceiver is not listening");
            return;
        }

        mCompanionDeviceManager.removeOnMessageReceivedListener(
            MESSAGE_TASK_CONTINUITY,
            mOnMessageReceivedListener);

        mIsListening = false;
    }

    private void onMessageReceived(int associationId, byte[] data) {
        Slog.v(TAG, "Received message from association id: " + associationId);
    }
}
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ import java.io.IOException;
 * message subclasses to support serialization and deserialization as part of
 * {@link TaskContinuityMessage}.
 */
interface TaskContinuityMessageData {
public interface TaskContinuityMessageData {

    /**
     * Writes this object to a proto output stream.
+165 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.never;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;

import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.companion.IOnTransportsChangedListener;
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceManager;
import android.companion.AssociationInfo;
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.messages.ContinuityDeviceConnected;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;

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

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

    private Context mMockContext;

    @Mock
    private ActivityTaskManager mMockActivityTaskManager;

    @Mock
    private ICompanionDeviceManager mMockCompanionDeviceManagerService;

    private CompanionDeviceManager mCompanionDeviceManager;

    private TaskBroadcaster mTaskBroadcaster;

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

        // Setup fake services.
        mCompanionDeviceManager
            = new CompanionDeviceManager(
                mMockCompanionDeviceManagerService,
                mMockContext);

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

        // Create TaskBroadcaster.
        mTaskBroadcaster = new TaskBroadcaster(mMockContext);
    }

    @Test
    public void testStopBroadcasting_doesNothingIfNotBroadcasting()
        throws Exception {

        mTaskBroadcaster.stopBroadcasting();
        verify(mMockCompanionDeviceManagerService, never())
            .removeOnTransportsChangedListener(any());
    }

    @Test
    public void testStartAndStopBroadcasting_updatesTransportsListener()
        throws Exception {

        // Start broadcasting, verifying a transport listener is added.
        ArgumentCaptor<IOnTransportsChangedListener> listenerCaptor
            = ArgumentCaptor.forClass(IOnTransportsChangedListener.class);
        mTaskBroadcaster.startBroadcasting();
        verify(mMockCompanionDeviceManagerService, times(1))
            .addOnTransportsChangedListener(
                listenerCaptor.capture());
        IOnTransportsChangedListener listener = listenerCaptor.getValue();
        assertThat(listener).isNotNull();

        // Stop broadcasting, verifying the transport listener is removed.
        mTaskBroadcaster.stopBroadcasting();
        verify(mMockCompanionDeviceManagerService, times(1))
            .removeOnTransportsChangedListener(listener);
    }

    @Test
    public void testStartBroadcasting_startsBroadcasting() throws Exception {
        // Start broadcasting, verifying a transport listener is added.
        ArgumentCaptor<IOnTransportsChangedListener> listenerCaptor
            = ArgumentCaptor.forClass(IOnTransportsChangedListener.class);
        mTaskBroadcaster.startBroadcasting();
        verify(mMockCompanionDeviceManagerService, times(1))
            .addOnTransportsChangedListener(
                listenerCaptor.capture());
        IOnTransportsChangedListener listener = listenerCaptor.getValue();

        // Setup a fake foreground task.
        ActivityManager.RunningTaskInfo taskInfo
            = new ActivityManager.RunningTaskInfo();
        taskInfo.taskId = 1;
        when(mMockActivityTaskManager.getTasks(Integer.MAX_VALUE, true))
            .thenReturn(Arrays.asList(taskInfo));

        // Add a new transport
        AssociationInfo associationInfo = new AssociationInfo.Builder(1, 0, "")
            .setDisplayName("test")
            .build();

        listener.onTransportsChanged(Arrays.asList(associationInfo));
        TestableLooper.get(this).processAllMessages();

        // Verify the message is sent.
        ArgumentCaptor<byte[]> messageCaptor
            = ArgumentCaptor.forClass(byte[].class);
        verify(mMockCompanionDeviceManagerService, times(1)).sendMessage(
            eq(CompanionDeviceManager.MESSAGE_TASK_CONTINUITY),
            messageCaptor.capture(),
            eq(new int[] {1}));
        TaskContinuityMessage taskContinuityMessage = new TaskContinuityMessage(
            messageCaptor.getValue());
        assertThat(taskContinuityMessage.getData()).isInstanceOf(
            ContinuityDeviceConnected.class);
        ContinuityDeviceConnected continuityDeviceConnected
            = (ContinuityDeviceConnected) taskContinuityMessage.getData();
        assertThat(continuityDeviceConnected.getCurrentForegroundTaskId())
            .isEqualTo(taskInfo.taskId);
    }
}
 No newline at end of file
Loading