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

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

Merge "[Handoff][1/N] Add InboundHandoffRequestController" into main

parents 334824ce e05dac39
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -25,8 +25,10 @@ import android.companion.datatransfer.continuity.RemoteTask;
import android.content.Context;
import android.util.Slog;

import com.android.server.companion.datatransfer.continuity.handoff.InboundHandoffRequestController;
import com.android.server.companion.datatransfer.continuity.handoff.OutboundHandoffRequestController;
import com.android.server.companion.datatransfer.continuity.messages.ContinuityDeviceConnected;
import com.android.server.companion.datatransfer.continuity.messages.HandoffRequestMessage;
import com.android.server.companion.datatransfer.continuity.messages.HandoffRequestResultMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskAddedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskRemovedMessage;
@@ -49,6 +51,7 @@ public final class TaskContinuityManagerService extends SystemService {

    private static final String TAG = "TaskContinuityManagerService";

    private InboundHandoffRequestController mInboundHandoffRequestController;
    private OutboundHandoffRequestController mOutboundHandoffRequestController;
    private TaskContinuityManagerServiceImpl mTaskContinuityManagerService;
    private TaskBroadcaster mTaskBroadcaster;
@@ -67,6 +70,7 @@ public final class TaskContinuityManagerService extends SystemService {
        mTaskContinuityMessageReceiver = new TaskContinuityMessageReceiver(context);
        mRemoteTaskStore = new RemoteTaskStore(mConnectedAssociationStore);
        mOutboundHandoffRequestController = new OutboundHandoffRequestController(context);
        mInboundHandoffRequestController = new InboundHandoffRequestController(context);
    }

    @Override
@@ -132,6 +136,11 @@ public final class TaskContinuityManagerService extends SystemService {
                    associationId,
                    handoffRequestResultMessage);
                break;
            case HandoffRequestMessage handoffRequestMessage:
                mInboundHandoffRequestController.onHandoffRequestMessageReceived(
                    associationId,
                    handoffRequestMessage);
                break;
            default:
                Slog.w(TAG, "Received unknown message from device: " + associationId);
                break;
+167 −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.handoff;

import static android.app.HandoffFailureCode.HANDOFF_FAILURE_TIMEOUT;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNSUPPORTED_DEVICE;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNSUPPORTED_TASK;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNKNOWN_TASK;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_INTERNAL_ERROR;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_TIMEOUT;
import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_TASK_CONTINUITY;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_SUCCESS;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_TIMEOUT;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_TASK_NOT_FOUND;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK;

import android.app.HandoffActivityData;
import android.app.IHandoffTaskDataReceiver;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.os.Binder;
import android.util.Slog;

import com.android.server.LocalServices;
import com.android.server.companion.datatransfer.continuity.messages.HandoffRequestMessage;
import com.android.server.companion.datatransfer.continuity.messages.HandoffRequestResultMessage;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;
import com.android.server.wm.ActivityTaskManagerInternal;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Responsible for receiving handoff requests from other devices and passing back data needed to
 * reinflate the tasks on the remote device.
 */
public class InboundHandoffRequestController extends IHandoffTaskDataReceiver.Stub {

    private static final String TAG = "InboundHandoffRequestController";

    // Map of task id to list of association ids that have a pending handoff request for that task.
    private final Map<Integer, List<Integer>> mPendingHandoffRequests = new HashMap<>();
    private final CompanionDeviceManager mCompanionDeviceManager;
    private final ActivityTaskManagerInternal mActivityTaskManagerInternal;

    public InboundHandoffRequestController(Context context) {
        mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
        mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
    }

    @Override
    public void onHandoffTaskDataRequestSucceeded(
        int taskId,
        List<HandoffActivityData> handoffActivityData) {
        final long ident = Binder.clearCallingIdentity();
        try {
            Slog.v(TAG, "onHandoffTaskDataRequestSucceeded for " + taskId);
            finishRequest(new HandoffRequestResultMessage(
                taskId,
                HANDOFF_REQUEST_RESULT_SUCCESS,
                handoffActivityData));
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    @Override
    public void onHandoffTaskDataRequestFailed(int taskId, int errorCode) {
        final long ident = Binder.clearCallingIdentity();
        try {
            Slog.v(TAG, "onHandoffTaskDataRequestFailed for " + taskId);
            finishRequest(new HandoffRequestResultMessage(
                taskId,
                getStatusCodeFromHandoffTaskDataReceiverCode(errorCode),
                List.of()));
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    public void onHandoffRequestMessageReceived(
        int associationId,
        HandoffRequestMessage handoffRequestMessage) {

        synchronized (mPendingHandoffRequests) {
            if (mPendingHandoffRequests.containsKey(handoffRequestMessage.taskId())) {
                // Add this request to the list of pending requests for this task.
                mPendingHandoffRequests
                    .get(handoffRequestMessage.taskId())
                    .add(associationId);
            } else {
                // Track this as a new request.
                List<Integer> associationIds = new ArrayList<>();
                associationIds.add(associationId);
                mPendingHandoffRequests.put(handoffRequestMessage.taskId(), associationIds);
                Slog.i(TAG, "Requesting handoff data for task " + handoffRequestMessage.taskId());
                mActivityTaskManagerInternal.requestHandoffTaskData(
                    handoffRequestMessage.taskId(),
                    this);
            }
        }
    }

    private void finishRequest(HandoffRequestResultMessage handoffRequestResultMessage) {
        synchronized (mPendingHandoffRequests) {
            if (!mPendingHandoffRequests.containsKey(handoffRequestResultMessage.taskId())) {
                Slog.w(TAG, "Received HandoffActivityData for task "
                    + handoffRequestResultMessage.taskId()
                    + ", but no pending request were found.");
                return;
            }

            List<Integer> associationIds = mPendingHandoffRequests
                .get(handoffRequestResultMessage.taskId());
            mPendingHandoffRequests.remove(handoffRequestResultMessage.taskId());

            TaskContinuityMessage taskContinuityMessage = new TaskContinuityMessage.Builder()
                .setData(handoffRequestResultMessage)
                .build();

            int[] associationIdsArray = new int[associationIds.size()];
            for (int i = 0; i < associationIds.size(); i++) {
                associationIdsArray[i] = associationIds.get(i);
            }

            Slog.i(TAG, "Sending result message to " + associationIds.size() + " associations.");
            try {
                mCompanionDeviceManager.sendMessage(
                    MESSAGE_ONEWAY_TASK_CONTINUITY,
                    taskContinuityMessage.toBytes(),
                    associationIdsArray);
            } catch (IOException e) {
                Slog.e(TAG, "Failed to send message to associations " + associationIds, e);
            }
        }
    }

    private static int getStatusCodeFromHandoffTaskDataReceiverCode(
        int handoffTaskDataReceiverCode) {

        switch (handoffTaskDataReceiverCode) {
            case HANDOFF_FAILURE_TIMEOUT:
                return HANDOFF_REQUEST_RESULT_FAILURE_TIMEOUT;
            case HANDOFF_FAILURE_UNKNOWN_TASK:
                return HANDOFF_REQUEST_RESULT_FAILURE_TASK_NOT_FOUND;
            default:
                return HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK;
        }
    }
}
 No newline at end of file
+266 −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.handoff;

import static android.app.HandoffFailureCode.HANDOFF_FAILURE_EMPTY_TASK;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_INTERNAL_ERROR;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_TIMEOUT;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNKNOWN_TASK;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNSUPPORTED_DEVICE;
import static android.app.HandoffFailureCode.HANDOFF_FAILURE_UNSUPPORTED_TASK;
import static android.companion.CompanionDeviceManager.MESSAGE_ONEWAY_TASK_CONTINUITY;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_TASK_NOT_FOUND;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_TIMEOUT;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_SUCCESS;

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

import android.app.HandoffActivityData;
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.IBinder;
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.HandoffRequestMessage;
import com.android.server.companion.datatransfer.continuity.messages.HandoffRequestResultMessage;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.LocalServices;

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

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

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

    @Mock
    private Context mMockContext;
    @Mock
    private ICompanionDeviceManager mMockCompanionDeviceManagerService;

    private ActivityTaskManagerInternal mMockActivityTaskManagerInternal;

    private CompanionDeviceManager mCompanionDeviceManager;
    private InboundHandoffRequestController mInboundHandoffRequestController;

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

        mCompanionDeviceManager = new CompanionDeviceManager(
                mMockCompanionDeviceManagerService, mMockContext);

        mMockActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class);

        when(mMockContext.getSystemService(CompanionDeviceManager.class))
                .thenReturn(mCompanionDeviceManager);
        LocalServices.addService(
            ActivityTaskManagerInternal.class,
            mMockActivityTaskManagerInternal);

        mInboundHandoffRequestController = new InboundHandoffRequestController(mMockContext);
    }

     @After
    public void unregisterLocalServices() throws Exception {
        LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class);
    }

    @Test
    public void onHandoffTaskDataRequestSucceeded_sendsSuccessMessage() throws Exception {
        int associationId = 1;
        int taskId = 2;

        // Setup a pending request
        mInboundHandoffRequestController.onHandoffRequestMessageReceived(
                associationId, new HandoffRequestMessage(taskId));
        verify(mMockActivityTaskManagerInternal, times(1))
                .requestHandoffTaskData(eq(taskId), eq(mInboundHandoffRequestController));

        HandoffActivityData handoffActivityData = new HandoffActivityData.Builder(
                new ComponentName("testPackage", "testActivity"))
            .build();
        List<HandoffActivityData> handoffData = List.of(handoffActivityData);
        mInboundHandoffRequestController.onHandoffTaskDataRequestSucceeded(taskId, handoffData);

        ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
        verify(mMockCompanionDeviceManagerService).sendMessage(
                eq(MESSAGE_ONEWAY_TASK_CONTINUITY),
                messageCaptor.capture(),
                any());

        TaskContinuityMessage sentMessage = new TaskContinuityMessage(messageCaptor.getValue());
        assertThat(sentMessage.getData()).isInstanceOf(HandoffRequestResultMessage.class);
        HandoffRequestResultMessage resultMessage =
                (HandoffRequestResultMessage) sentMessage.getData();
        assertThat(resultMessage.taskId()).isEqualTo(taskId);
        assertThat(resultMessage.statusCode()).isEqualTo(HANDOFF_REQUEST_RESULT_SUCCESS);
        assertThat(resultMessage.activities()).hasSize(1);
        assertThat(resultMessage.activities().get(0).getComponentName())
                .isEqualTo(handoffActivityData.getComponentName());
    }

    @Test
    public void onHandoffTaskDataRequestSucceeded_multipleAssociations_sendsToAll()
            throws Exception {

        int firstAssociationId = 1;
        int secondAssociationId = 2;
        int taskId = 3;

        // Setup pending requests from two associations for the same task
        mInboundHandoffRequestController.onHandoffRequestMessageReceived(
                firstAssociationId, new HandoffRequestMessage(taskId));
        mInboundHandoffRequestController.onHandoffRequestMessageReceived(
                secondAssociationId, new HandoffRequestMessage(taskId));
        // requestHandoffTaskData should only be called once for the task
        verify(mMockActivityTaskManagerInternal, times(1))
                .requestHandoffTaskData(eq(taskId), any());

        HandoffActivityData handoffActivityData = new HandoffActivityData.Builder(
            new ComponentName("testPackage", "testActivity"))
        .build();

        List<HandoffActivityData> handoffData = List.of(handoffActivityData);
        mInboundHandoffRequestController.onHandoffTaskDataRequestSucceeded(taskId, handoffData);

        ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
        ArgumentCaptor<int[]> associationIdsCaptor = ArgumentCaptor.forClass(int[].class);

        verify(mMockCompanionDeviceManagerService).sendMessage(
                eq(MESSAGE_ONEWAY_TASK_CONTINUITY),
                messageCaptor.capture(),
                associationIdsCaptor.capture());

        TaskContinuityMessage sentMessage = new TaskContinuityMessage(messageCaptor.getValue());
        assertThat(sentMessage.getData()).isInstanceOf(HandoffRequestResultMessage.class);
        HandoffRequestResultMessage resultMessage =
                (HandoffRequestResultMessage) sentMessage.getData();
        assertThat(resultMessage.taskId()).isEqualTo(taskId);
        assertThat(resultMessage.statusCode()).isEqualTo(HANDOFF_REQUEST_RESULT_SUCCESS);
        assertThat(resultMessage.activities()).hasSize(1);
        assertThat(resultMessage.activities().get(0).getComponentName())
                .isEqualTo(handoffActivityData.getComponentName());

        assertThat(associationIdsCaptor.getValue()).asList()
                .containsExactly(firstAssociationId, secondAssociationId);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_timeout() throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_TIMEOUT, HANDOFF_REQUEST_RESULT_FAILURE_TIMEOUT);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_unknownTask() throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_UNKNOWN_TASK, HANDOFF_REQUEST_RESULT_FAILURE_TASK_NOT_FOUND);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_unsupportedTask()
            throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_UNSUPPORTED_TASK,
                HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_emptyTask() throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_EMPTY_TASK,
                HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_unsupportedDevice()
            throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_UNSUPPORTED_DEVICE,
                HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK);
    }

    @Test
    public void onHandoffTaskDataRequestFailed_sendsFailureMessage_internalError()
            throws Exception {
        testHandoffFailure(
                HANDOFF_FAILURE_INTERNAL_ERROR,
                HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK);
    }


    private void testHandoffFailure(int receiverErrorCode, int expectedStatusCode)
            throws Exception {

        int associationId = 1;
        int taskId = 1;

        // Setup a pending request
        mInboundHandoffRequestController.onHandoffRequestMessageReceived(
                associationId, new HandoffRequestMessage(taskId));
        verify(mMockActivityTaskManagerInternal, times(1))
                .requestHandoffTaskData(eq(taskId), any());

        mInboundHandoffRequestController.onHandoffTaskDataRequestFailed(taskId, receiverErrorCode);

        ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
        verify(mMockCompanionDeviceManagerService).sendMessage(
                eq(MESSAGE_ONEWAY_TASK_CONTINUITY),
                messageCaptor.capture(),
                eq(new int[]{associationId}));

        TaskContinuityMessage sentMessage = new TaskContinuityMessage(messageCaptor.getValue());
        assertThat(sentMessage.getData()).isInstanceOf(HandoffRequestResultMessage.class);
        HandoffRequestResultMessage resultMessage =
                (HandoffRequestResultMessage) sentMessage.getData();
        assertThat(resultMessage.taskId()).isEqualTo(taskId);
        assertThat(resultMessage.statusCode()).isEqualTo(expectedStatusCode);
        assertThat(resultMessage.activities()).isEmpty();
    }
}
 No newline at end of file