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

Commit c4d9d988 authored by Joe Antonetti's avatar Joe Antonetti Committed by Android (Google) Code Review
Browse files

Merge "Create TaskBroadcaster" into main

parents 7f36a510 d1d95253
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