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

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

Merge "[Handoff][3/N] Use RemoteCallbackList for Handoff Callback storage." into main

parents c0bbea00 7ec26b1f
Loading
Loading
Loading
Loading
+108 −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 android.annotation.NonNull;
import android.companion.datatransfer.continuity.IHandoffRequestCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

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

class HandoffRequestCallbackHolder {

    private static final String TAG = "HandoffRequestCallbackHolder";

    @GuardedBy("mCallbacks")
    private final RemoteCallbackList<IHandoffRequestCallback> mCallbacks
        = new RemoteCallbackList<>();

    private record RequestCookie(int associationId, int taskId) {}

    /**
     * Registers a callback for the given association and task.
     *
     * @param associationId The association ID of the handoff request.
     * @param taskId The task ID of the handoff request.
     * @param callback The callback to register.
     */
    public void registerCallback(
        int associationId,
        int taskId,
        @NonNull IHandoffRequestCallback callback) {

        Objects.requireNonNull(callback);
        synchronized (mCallbacks) {
            Slog.i(
                TAG,
                "Registering HandoffRequestCallback for association " + associationId + " and task "
                + taskId);

            mCallbacks.register(callback, new RequestCookie(associationId, taskId));
        }
    }

    /**
     * Notifies all callbacks for the given association and task, and removes them from the list of
     * pending callbacks.
     *
     * @param associationId The association ID of the handoff request.
     * @param taskId The task ID of the handoff request.
     * @param statusCode The status code of the handoff request.
     */
    public void notifyAndRemoveCallbacks(int associationId, int taskId, int statusCode) {
        synchronized (mCallbacks) {
            Slog.i(
                TAG,
                "Notifying HandoffRequestCallbacks for association " + associationId + " and task "
                + taskId + " of status code " + statusCode);

            RequestCookie request = new RequestCookie(associationId, taskId);
            List<IHandoffRequestCallback> callbacksToRemove = new ArrayList<>();
            mCallbacks.broadcast(
                (callback, cookie) -> {
                    if (request.equals(cookie)) {
                        try {
                            callback.onHandoffRequestFinished(associationId, taskId, statusCode);
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Failed to notify callback of handoff request result", e);
                        }

                        callbacksToRemove.add(callback);
                    }
                }
            );

            clearCallbacks(callbacksToRemove);
        }
    }

    private void clearCallbacks(@NonNull List<IHandoffRequestCallback> callbacks) {
        Objects.requireNonNull(callbacks);
        synchronized (mCallbacks) {
            Slog.i(TAG, "Clearing " + callbacks.size() + " callbacks.");
            for (IHandoffRequestCallback callback : callbacks) {
                mCallbacks.unregister(callback);
            }
        }
    }
}
 No newline at end of file
+29 −44
Original line number Diff line number Diff line
@@ -27,11 +27,10 @@ import com.android.server.companion.datatransfer.continuity.messages.HandoffRequ
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessage;
import com.android.server.companion.datatransfer.continuity.messages.TaskContinuityMessageSerializer;
import com.android.server.companion.datatransfer.continuity.handoff.HandoffActivityStarter;
import com.android.server.companion.datatransfer.continuity.handoff.HandoffRequestCallbackHolder;

import android.app.ActivityOptions;
import android.app.HandoffActivityData;
import android.content.Context;
import android.content.Intent;
import android.companion.CompanionDeviceManager;
import android.companion.datatransfer.continuity.IHandoffRequestCallback;
import android.os.Bundle;
@@ -40,10 +39,8 @@ import android.util.Slog;
import android.os.UserHandle;

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

/**
 * Controller for outbound handoff requests.
@@ -55,11 +52,14 @@ public class OutboundHandoffRequestController {

    private static final String TAG = "OutboundHandoffRequestController";

    private record PendingHandoffRequest(int associationId, int taskId) {}

    private final Context mContext;
    private final CompanionDeviceManager mCompanionDeviceManager;
    private final ConnectedAssociationStore mConnectedAssociationStore;
    private final Map<Integer, Map<Integer, List<IHandoffRequestCallback>>> mPendingCallbacks
        = new HashMap<>();
    private final HandoffRequestCallbackHolder mHandoffRequestCallbackHolder
        = new HandoffRequestCallbackHolder();
    private final Set<PendingHandoffRequest> mPendingHandoffRequests = new HashSet<>();

    public OutboundHandoffRequestController(
        Context context,
@@ -84,19 +84,14 @@ public class OutboundHandoffRequestController {
            return;
        }

        synchronized (mPendingCallbacks) {
            if (!mPendingCallbacks.containsKey(associationId)) {
                mPendingCallbacks.put(associationId, new HashMap<>());
            }

            if (mPendingCallbacks.get(associationId).containsKey(taskId)) {
                mPendingCallbacks.get(associationId).get(taskId).add(callback);
        synchronized (mPendingHandoffRequests) {
            PendingHandoffRequest request = new PendingHandoffRequest(associationId, taskId);
            if (mPendingHandoffRequests.contains(request)) {
                mHandoffRequestCallbackHolder.registerCallback(associationId, taskId, callback);
                return;
            }

            List<IHandoffRequestCallback> callbacks = new ArrayList<>();
            callbacks.add(callback);
            mPendingCallbacks.get(associationId).put(taskId, callbacks);
            mPendingHandoffRequests.add(request);
            HandoffRequestMessage handoffRequestMessage = new HandoffRequestMessage(taskId);
            try {
                mCompanionDeviceManager.sendMessage(
@@ -105,7 +100,10 @@ public class OutboundHandoffRequestController {
                    new int[] {associationId});
            } catch (IOException e) {
                Slog.e(TAG, "Failed to send handoff request message to device " + associationId, e);
                return;
            }

            mHandoffRequestCallbackHolder.registerCallback(associationId, taskId, callback);
        }
    }

@@ -113,19 +111,18 @@ public class OutboundHandoffRequestController {
        int associationId,
        HandoffRequestResultMessage handoffRequestResultMessage) {

        synchronized (mPendingCallbacks) {
            if (handoffRequestResultMessage.statusCode() != HANDOFF_REQUEST_RESULT_SUCCESS) {
                finishHandoffRequest(
                    associationId,
                    handoffRequestResultMessage.taskId(),
                    handoffRequestResultMessage.statusCode());
        synchronized (mPendingHandoffRequests) {
            PendingHandoffRequest request
                = new PendingHandoffRequest(associationId, handoffRequestResultMessage.taskId());
            if (!mPendingHandoffRequests.contains(request)) {
                return;
            }

            if (handoffRequestResultMessage.activities().isEmpty()) {
            if (handoffRequestResultMessage.statusCode() != HANDOFF_REQUEST_RESULT_SUCCESS) {
                finishHandoffRequest(
                    associationId,
                    handoffRequestResultMessage.taskId(),
                    HANDOFF_REQUEST_RESULT_FAILURE_NO_DATA_PROVIDED_BY_TASK);
                    handoffRequestResultMessage.statusCode());
                return;
            }

@@ -148,27 +145,15 @@ public class OutboundHandoffRequestController {
    }

    private void finishHandoffRequest(int associationId, int taskId, int statusCode) {
        synchronized (mPendingCallbacks) {
            if (!mPendingCallbacks.containsKey(associationId)) {
                return;
            }

            Map<Integer, List<IHandoffRequestCallback>> pendingCallbacksForAssociation =
                mPendingCallbacks.get(associationId);

            if (!pendingCallbacksForAssociation.containsKey(taskId)) {
        synchronized (mPendingHandoffRequests) {
            PendingHandoffRequest request = new PendingHandoffRequest(associationId, taskId);
            if (!mPendingHandoffRequests.contains(request)) {
                return;
            }

            for (IHandoffRequestCallback callback : pendingCallbacksForAssociation.get(taskId)) {
                try {
                    callback.onHandoffRequestFinished(associationId, taskId, statusCode);
                } catch (RemoteException e) {
                    Slog.e(TAG, "Failed to notify callback of handoff request result", e);
                }
            }

            pendingCallbacksForAssociation.remove(taskId);
            mPendingHandoffRequests.remove(request);
            mHandoffRequestCallbackHolder
                .notifyAndRemoveCallbacks(associationId, taskId, statusCode);
        }
    }
}
 No newline at end of file
+55 −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 com.google.common.truth.Truth.assertThat;

import android.companion.datatransfer.continuity.IHandoffRequestCallback;
import android.os.RemoteException;

import java.util.ArrayList;
import java.util.List;

public final class FakeHandoffRequestCallback extends IHandoffRequestCallback.Stub {

    final List<Integer> receivedAssociationIds = new ArrayList<>();
    final List<Integer> receivedTaskIds = new ArrayList<>();
    final List<Integer> receivedResultCodes = new ArrayList<>();

    @Override
    public void onHandoffRequestFinished(
        int associationId,
        int remoteTaskId,
        int resultCode) throws RemoteException {

        receivedAssociationIds.add(associationId);
        receivedTaskIds.add(remoteTaskId);
        receivedResultCodes.add(resultCode);
    }

    void verifyInvoked(int associationId, int taskId, int resultCode) {
        assertThat(receivedAssociationIds).containsExactly(associationId);
        assertThat(receivedTaskIds).containsExactly(taskId);
        assertThat(receivedResultCodes).containsExactly(resultCode);
    }

    void verifyNotInvoked() {
        assertThat(receivedAssociationIds).isEmpty();
        assertThat(receivedTaskIds).isEmpty();
        assertThat(receivedResultCodes).isEmpty();
    }
}
+114 −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.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_FAILURE_TIMEOUT;
import static android.companion.datatransfer.continuity.TaskContinuityManager.HANDOFF_REQUEST_RESULT_SUCCESS;

import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.companion.datatransfer.continuity.IHandoffRequestCallback;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;

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

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

    private HandoffRequestCallbackHolder mCallbackHolder;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mCallbackHolder = new HandoffRequestCallbackHolder();
    }

    @Test
    public void notifyAndRemoveCallbacks_notifiesAndRemoves() throws RemoteException {
        int associationId = 1;
        int taskId = 101;
        FakeHandoffRequestCallback callback = new FakeHandoffRequestCallback();
        mCallbackHolder.registerCallback(associationId, taskId, callback);

        mCallbackHolder.notifyAndRemoveCallbacks(
                associationId, taskId, HANDOFF_REQUEST_RESULT_SUCCESS);

        callback.verifyInvoked(associationId, taskId, HANDOFF_REQUEST_RESULT_SUCCESS);
    }

    @Test
    public void notifyAndRemoveCallbacks_notifiesCorrectCallback() throws RemoteException {
        int associationId1 = 1;
        int taskId1 = 101;
        int associationId2 = 2;
        int taskId2 = 202;
        FakeHandoffRequestCallback callback1 = new FakeHandoffRequestCallback();
        FakeHandoffRequestCallback callback2 = new FakeHandoffRequestCallback();
        mCallbackHolder.registerCallback(associationId1, taskId1, callback1);
        mCallbackHolder.registerCallback(associationId2, taskId2, callback2);

        mCallbackHolder.notifyAndRemoveCallbacks(
                associationId1, taskId1, HANDOFF_REQUEST_RESULT_SUCCESS);

        callback1.verifyInvoked(associationId1, taskId1, HANDOFF_REQUEST_RESULT_SUCCESS);
        callback2.verifyNotInvoked();
    }

    @Test
    public void notifyAndRemoveCallbacks_multipleCallbacksForSameRequest() throws RemoteException {
        int associationId = 1;
        int taskId = 101;
        FakeHandoffRequestCallback callback1 = new FakeHandoffRequestCallback();
        FakeHandoffRequestCallback callback2 = new FakeHandoffRequestCallback();
        IHandoffRequestCallback anotherCallbackForSameRequest = mock(IHandoffRequestCallback.class);
        mCallbackHolder.registerCallback(associationId, taskId, callback1);
        mCallbackHolder.registerCallback(associationId, taskId, callback2);

        mCallbackHolder.notifyAndRemoveCallbacks(
                associationId, taskId, HANDOFF_REQUEST_RESULT_SUCCESS);

        callback1.verifyInvoked(associationId, taskId, HANDOFF_REQUEST_RESULT_SUCCESS);
        callback2.verifyInvoked(associationId, taskId, HANDOFF_REQUEST_RESULT_SUCCESS);
    }

    @Test
    public void notifyAndRemoveCallbacks_noMatchingCallback() throws RemoteException {
        int associationId = 1;
        int taskId = 101;
        FakeHandoffRequestCallback callback = new FakeHandoffRequestCallback();
        mCallbackHolder.registerCallback(associationId, taskId, callback);

        mCallbackHolder.notifyAndRemoveCallbacks(
                associationId, 100, HANDOFF_REQUEST_RESULT_SUCCESS);

        callback.verifyNotInvoked();
    }
}
 No newline at end of file
+12 −43
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ public class OutboundHandoffRequestControllerTest {
    public void testRequestHandoff_success() throws Exception {
        int associationId = 1;
        int taskId = 1;
        HandoffRequestCallbackHolder callbackHolder = new HandoffRequestCallbackHolder();
        FakeHandoffRequestCallback callbackHolder = new FakeHandoffRequestCallback();
        AssociationInfo mockAssociationInfo = createAssociationInfo(associationId, "device");
        when(mMockConnectedAssociationStore.getConnectedAssociationById(associationId))
            .thenReturn(mockAssociationInfo);
@@ -97,7 +97,7 @@ public class OutboundHandoffRequestControllerTest {
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            callbackHolder.callback);
            callbackHolder);

        // Verify HandoffRequestMessage was sent.
        HandoffRequestMessage expectedHandoffRequestMessage = new HandoffRequestMessage(taskId);
@@ -152,13 +152,13 @@ public class OutboundHandoffRequestControllerTest {
    public void testRequestHandoff_associationNotConnected_returnsFailure() {
        int associationId = 1;
        int taskId = 1;
        HandoffRequestCallbackHolder callbackHolder = new HandoffRequestCallbackHolder();
        FakeHandoffRequestCallback callbackHolder = new FakeHandoffRequestCallback();
        when(mMockConnectedAssociationStore.getConnectedAssociationById(associationId))
            .thenReturn(null);
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            callbackHolder.callback);
            callbackHolder);

        // Verify the callback was invoked.
        callbackHolder.verifyInvoked(
@@ -176,16 +176,16 @@ public class OutboundHandoffRequestControllerTest {
            .thenReturn(mockAssociationInfo);

        // Request handoff multiple times.
        HandoffRequestCallbackHolder firstCallback = new HandoffRequestCallbackHolder();
        HandoffRequestCallbackHolder secondCallback = new HandoffRequestCallbackHolder();
        FakeHandoffRequestCallback firstCallback = new FakeHandoffRequestCallback();
        FakeHandoffRequestCallback secondCallback = new FakeHandoffRequestCallback();
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            firstCallback.callback);
            firstCallback);
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            secondCallback.callback);
            secondCallback);

        HandoffRequestMessage expectedHandoffRequestMessage = new HandoffRequestMessage(taskId);
        verify(mMockCompanionDeviceManagerService, times(1)).sendMessage(
@@ -202,11 +202,11 @@ public class OutboundHandoffRequestControllerTest {
        AssociationInfo mockAssociationInfo = createAssociationInfo(associationId, "device");
        when(mMockConnectedAssociationStore.getConnectedAssociationById(associationId))
            .thenReturn(mockAssociationInfo);
        HandoffRequestCallbackHolder callback = new HandoffRequestCallbackHolder();
        FakeHandoffRequestCallback callback = new FakeHandoffRequestCallback();
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            callback.callback);
            callback);

        // Simulate a message failure
        int failureStatusCode =
@@ -233,11 +233,11 @@ public class OutboundHandoffRequestControllerTest {
        AssociationInfo mockAssociationInfo = createAssociationInfo(associationId, "device");
        when(mMockConnectedAssociationStore.getConnectedAssociationById(associationId))
            .thenReturn(mockAssociationInfo);
        HandoffRequestCallbackHolder callback = new HandoffRequestCallbackHolder();
        FakeHandoffRequestCallback callback = new FakeHandoffRequestCallback();
        mOutboundHandoffRequestController.requestHandoff(
            associationId,
            taskId,
            callback.callback);
            callback);

        // Return no data for this request.
        mOutboundHandoffRequestController.onHandoffRequestResultMessageReceived(
@@ -256,35 +256,4 @@ public class OutboundHandoffRequestControllerTest {
        // Verify no intent was launched.
        verify(mContext, never()).startActivitiesAsUser(any(), any(), any());
    }

    private final class HandoffRequestCallbackHolder {

        final List<Integer> receivedAssociationIds = new ArrayList<>();
        final List<Integer> receivedTaskIds = new ArrayList<>();
        final List<Integer> receivedResultCodes = new ArrayList<>();
        final IHandoffRequestCallback callback = new IHandoffRequestCallback.Stub() {
            @Override
            public void onHandoffRequestFinished(
                int associationId,
                int remoteTaskId,
                int resultCode) throws RemoteException {

                receivedAssociationIds.add(associationId);
                receivedTaskIds.add(remoteTaskId);
                receivedResultCodes.add(resultCode);
            }
        };

        void verifyInvoked(int associationId, int taskId, int resultCode) {
            assertThat(receivedAssociationIds).containsExactly(associationId);
            assertThat(receivedTaskIds).containsExactly(taskId);
            assertThat(receivedResultCodes).containsExactly(resultCode);
        }

        void verifyNotInvoked() {
            assertThat(receivedAssociationIds).isEmpty();
            assertThat(receivedTaskIds).isEmpty();
            assertThat(receivedResultCodes).isEmpty();
        }
    }
}
 No newline at end of file