Loading services/companion/java/com/android/server/companion/datatransfer/continuity/handoff/HandoffRequestCallbackHolder.java 0 → 100644 +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 services/companion/java/com/android/server/companion/datatransfer/continuity/handoff/OutboundHandoffRequestController.java +29 −44 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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, Loading @@ -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( Loading @@ -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); } } Loading @@ -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; } Loading @@ -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 services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/FakeHandoffRequestCallback.java 0 → 100644 +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(); } } services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/HandoffRequestCallbackHolderTest.java 0 → 100644 +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 services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/OutboundHandoffRequestControllerTest.java +12 −43 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -97,7 +97,7 @@ public class OutboundHandoffRequestControllerTest { mOutboundHandoffRequestController.requestHandoff( associationId, taskId, callbackHolder.callback); callbackHolder); // Verify HandoffRequestMessage was sent. HandoffRequestMessage expectedHandoffRequestMessage = new HandoffRequestMessage(taskId); Loading Loading @@ -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( Loading @@ -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( Loading @@ -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 = Loading @@ -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( Loading @@ -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 Loading
services/companion/java/com/android/server/companion/datatransfer/continuity/handoff/HandoffRequestCallbackHolder.java 0 → 100644 +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
services/companion/java/com/android/server/companion/datatransfer/continuity/handoff/OutboundHandoffRequestController.java +29 −44 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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, Loading @@ -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( Loading @@ -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); } } Loading @@ -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; } Loading @@ -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
services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/FakeHandoffRequestCallback.java 0 → 100644 +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(); } }
services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/HandoffRequestCallbackHolderTest.java 0 → 100644 +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
services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/handoff/OutboundHandoffRequestControllerTest.java +12 −43 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -97,7 +97,7 @@ public class OutboundHandoffRequestControllerTest { mOutboundHandoffRequestController.requestHandoff( associationId, taskId, callbackHolder.callback); callbackHolder); // Verify HandoffRequestMessage was sent. HandoffRequestMessage expectedHandoffRequestMessage = new HandoffRequestMessage(taskId); Loading Loading @@ -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( Loading @@ -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( Loading @@ -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 = Loading @@ -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( Loading @@ -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