From f64304aa6de56f5244e46fabb0279b6085a2b834 Mon Sep 17 00:00:00 2001 From: Christine Franks Date: Tue, 25 Apr 2023 13:53:26 -0700 Subject: [PATCH] Send / recv contextsync messages via securechannel Bug: 265466098 Test: atest FrameworksServicesTests:com.android.server.companion.datatransfer Change-Id: I3e17e44b47b9592c3fe7d8f18fa3ad1dda16d786 --- core/proto/android/companion/telecom.proto | 10 +- .../CompanionDeviceManagerService.java | 41 ++ ...CompanionDeviceManagerServiceInternal.java | 31 +- .../CallMetadataSyncInCallService.java | 193 ++++++--- .../CrossDeviceSyncController.java | 388 +++++++++++++----- ...=> CrossDeviceSyncControllerCallback.java} | 10 +- .../CrossDeviceSyncControllerTest.java | 138 +++++++ 7 files changed, 625 insertions(+), 186 deletions(-) rename services/companion/java/com/android/server/companion/datatransfer/contextsync/{CallMetadataSyncCallback.java => CrossDeviceSyncControllerCallback.java} (67%) create mode 100644 services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java diff --git a/core/proto/android/companion/telecom.proto b/core/proto/android/companion/telecom.proto index 9ccadbf6eb2d8..3a9e5eeb48777 100644 --- a/core/proto/android/companion/telecom.proto +++ b/core/proto/android/companion/telecom.proto @@ -20,9 +20,9 @@ package android.companion; option java_multiple_files = true; -// Next index: 2 +// Next index: 4 message Telecom { - // Next index: 5 + // Next index: 6 message Call { // UUID representing this call int64 id = 1; @@ -34,6 +34,8 @@ message Telecom { // Human-readable name of the app processing this call string app_name = 2; bytes app_icon = 3; + // Unique identifier for this app, such as a package name. + string app_identifier = 4; } Origin origin = 2; @@ -59,9 +61,11 @@ message Telecom { REJECT_AND_BLOCK = 9; IGNORE = 10; } - repeated Control controls_available = 4; + repeated Control controls = 4; } // The list of active calls. repeated Call calls = 1; + // The list of requested calls or call changes. + repeated Call requests = 2; } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a3a0674810223..8545c9b83cc6a 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -108,6 +108,9 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.pm.UserManagerInternal; @@ -117,6 +120,7 @@ import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -200,6 +204,8 @@ public class CompanionDeviceManagerService extends SystemService { private final RemoteCallbackList mListeners = new RemoteCallbackList<>(); + private CrossDeviceSyncController mCrossDeviceSyncController; + public CompanionDeviceManagerService(Context context) { super(context); @@ -239,6 +245,8 @@ public class CompanionDeviceManagerService extends SystemService { mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); + // TODO(b/279663946): move context sync to a dedicated system service + mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); // Publish "binder" service. final CompanionDeviceManagerImpl impl = new CompanionDeviceManagerImpl(); @@ -1369,6 +1377,39 @@ public class CompanionDeviceManagerService extends SystemService { public void removeInactiveSelfManagedAssociations() { CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations(); } + + @Override + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.registerCallMetadataSyncCallback(callback); + } + } + + @Override + public void crossDeviceSync(int userId, Collection calls) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncToAllDevicesForUserId(userId, calls); + } + } + + @Override + public void crossDeviceSync(AssociationInfo associationInfo, + Collection calls) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncToSingleDevice(associationInfo, calls); + } + } + + @Override + public void sendCrossDeviceSyncMessage(int associationId, byte[] message) { + if (CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCrossDeviceSyncController.syncMessageToDevice(associationId, message); + } + } } /** diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index 36492407d4639..3b108e63e13d8 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -16,12 +16,41 @@ package com.android.server.companion; +import android.companion.AssociationInfo; + +import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; +import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; + +import java.util.Collection; + /** * Companion Device Manager Local System Service Interface. */ -interface CompanionDeviceManagerServiceInternal { +public interface CompanionDeviceManagerServiceInternal { /** * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations */ void removeInactiveSelfManagedAssociations(); + + /** + * Registers a callback from an InCallService / ConnectionService to CDM to process sync + * requests and perform call control actions. + */ + void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback); + + /** + * Requests a sync from an InCallService / ConnectionService to CDM, for the given association + * and message. + */ + void sendCrossDeviceSyncMessage(int associationId, byte[] message); + + /** + * Requests a sync from an InCallService to CDM, for the given user and call metadata. + */ + void crossDeviceSync(int userId, Collection calls); + + /** + * Requests a sync from an InCallService to CDM, for the given association and call metadata. + */ + void crossDeviceSync(AssociationInfo associationInfo, Collection calls); } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java index ae4766ac9fda4..443a732eb6f19 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java @@ -17,15 +17,20 @@ package com.android.server.companion.datatransfer.contextsync; import android.annotation.Nullable; +import android.companion.AssociationInfo; import android.telecom.Call; import android.telecom.InCallService; import android.telecom.TelecomManager; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; import com.android.server.companion.CompanionDeviceConfig; +import com.android.server.companion.CompanionDeviceManagerServiceInternal; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.stream.Collectors; @@ -35,90 +40,132 @@ import java.util.stream.Collectors; */ public class CallMetadataSyncInCallService extends InCallService { + private static final String TAG = "CallMetadataIcs"; private static final long NOT_VALID = -1L; + private CompanionDeviceManagerServiceInternal mCdmsi; + @VisibleForTesting final Map mCurrentCalls = new HashMap<>(); - @VisibleForTesting - boolean mShouldSync; + @VisibleForTesting int mNumberOfActiveSyncAssociations; final Call.Callback mTelecomCallback = new Call.Callback() { @Override public void onDetailsChanged(Call call, Call.Details details) { - mCurrentCalls.get(call).updateCallDetails(details); - } - }; - final CallMetadataSyncCallback mCallMetadataSyncCallback = new CallMetadataSyncCallback() { - @Override - void processCallControlAction(int crossDeviceCallId, int callControlAction) { - final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId, - mCurrentCalls.values()); - switch (callControlAction) { - case android.companion.Telecom.Call.ACCEPT: - if (crossDeviceCall != null) { - crossDeviceCall.doAccept(); - } - break; - case android.companion.Telecom.Call.REJECT: - if (crossDeviceCall != null) { - crossDeviceCall.doReject(); - } - break; - case android.companion.Telecom.Call.SILENCE: - doSilence(); - break; - case android.companion.Telecom.Call.MUTE: - doMute(); - break; - case android.companion.Telecom.Call.UNMUTE: - doUnmute(); - break; - case android.companion.Telecom.Call.END: - if (crossDeviceCall != null) { - crossDeviceCall.doEnd(); - } - break; - case android.companion.Telecom.Call.PUT_ON_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doPutOnHold(); - } - break; - case android.companion.Telecom.Call.TAKE_OFF_HOLD: - if (crossDeviceCall != null) { - crossDeviceCall.doTakeOffHold(); - } - break; - default: - } - } - - @Override - void requestCrossDeviceSync(int userId) { - } - - @Override - void updateStatus(int userId, boolean shouldSyncCallMetadata) { - if (userId == getUserId()) { - mShouldSync = shouldSyncCallMetadata; - if (shouldSyncCallMetadata) { - initializeCalls(); + if (mNumberOfActiveSyncAssociations > 0) { + final CrossDeviceCall crossDeviceCall = mCurrentCalls.get(call); + if (crossDeviceCall != null) { + crossDeviceCall.updateCallDetails(details); + sync(getUserId()); } else { - mCurrentCalls.clear(); + Slog.w(TAG, "Could not update details for nonexistent call"); } } } }; + final CrossDeviceSyncControllerCallback + mCrossDeviceSyncControllerCallback = new CrossDeviceSyncControllerCallback() { + @Override + void processContextSyncMessage(int associationId, + CallMetadataSyncData callMetadataSyncData) { + final Iterator iterator = + callMetadataSyncData.getRequests().iterator(); + while (iterator.hasNext()) { + final CallMetadataSyncData.Call call = iterator.next(); + if (call.getId() != 0) { + // The call is already assigned an id; treat as control invocations. + for (int control : call.getControls()) { + processCallControlAction(call.getId(), control); + } + } + iterator.remove(); + } + } + + private void processCallControlAction(long crossDeviceCallId, + int callControlAction) { + final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId, + mCurrentCalls.values()); + switch (callControlAction) { + case android.companion.Telecom.Call.ACCEPT: + if (crossDeviceCall != null) { + crossDeviceCall.doAccept(); + } + break; + case android.companion.Telecom.Call.REJECT: + if (crossDeviceCall != null) { + crossDeviceCall.doReject(); + } + break; + case android.companion.Telecom.Call.SILENCE: + doSilence(); + break; + case android.companion.Telecom.Call.MUTE: + doMute(); + break; + case android.companion.Telecom.Call.UNMUTE: + doUnmute(); + break; + case android.companion.Telecom.Call.END: + if (crossDeviceCall != null) { + crossDeviceCall.doEnd(); + } + break; + case android.companion.Telecom.Call.PUT_ON_HOLD: + if (crossDeviceCall != null) { + crossDeviceCall.doPutOnHold(); + } + break; + case android.companion.Telecom.Call.TAKE_OFF_HOLD: + if (crossDeviceCall != null) { + crossDeviceCall.doTakeOffHold(); + } + break; + default: + } + } + + @Override + void requestCrossDeviceSync(AssociationInfo associationInfo) { + if (associationInfo.getUserId() == getUserId()) { + sync(associationInfo); + } + } + + @Override + void updateNumberOfActiveSyncAssociations(int userId, boolean added) { + if (userId == getUserId()) { + final boolean wasActivelySyncing = mNumberOfActiveSyncAssociations > 0; + if (added) { + mNumberOfActiveSyncAssociations++; + } else { + mNumberOfActiveSyncAssociations--; + } + if (!wasActivelySyncing && mNumberOfActiveSyncAssociations > 0) { + initializeCalls(); + } else if (wasActivelySyncing && mNumberOfActiveSyncAssociations <= 0) { + mCurrentCalls.clear(); + } + } + } + }; @Override public void onCreate() { super.onCreate(); - initializeCalls(); + if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class); + mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback); + } } private void initializeCalls() { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.putAll(getCalls().stream().collect(Collectors.toMap(call -> call, call -> new CrossDeviceCall(getPackageManager(), call, getCallAudioState())))); + mCurrentCalls.keySet().forEach(call -> call.registerCallback(mTelecomCallback, + getMainThreadHandler())); + sync(getUserId()); } } @@ -139,33 +186,39 @@ public class CallMetadataSyncInCallService extends InCallService { @Override public void onCallAdded(Call call) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.put(call, new CrossDeviceCall(getPackageManager(), call, getCallAudioState())); + call.registerCallback(mTelecomCallback); + sync(getUserId()); } } @Override public void onCallRemoved(Call call) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.remove(call); + call.unregisterCallback(mTelecomCallback); + sync(getUserId()); } } @Override public void onMuteStateChanged(boolean isMuted) { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.values().forEach(call -> call.updateMuted(isMuted)); + sync(getUserId()); } } @Override public void onSilenceRinger() { if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM) - && mShouldSync) { + && mNumberOfActiveSyncAssociations > 0) { mCurrentCalls.values().forEach(call -> call.updateSilencedIfRinging()); + sync(getUserId()); } } @@ -183,4 +236,12 @@ public class CallMetadataSyncInCallService extends InCallService { telecomManager.silenceRinger(); } } + + private void sync(int userId) { + mCdmsi.crossDeviceSync(userId, mCurrentCalls.values()); + } + + private void sync(AssociationInfo associationInfo) { + mCdmsi.crossDeviceSync(associationInfo, mCurrentCalls.values()); + } } \ No newline at end of file diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java index 3d8fb7a8d5bfe..adc5faf24f2c7 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java @@ -16,27 +16,32 @@ package com.android.server.companion.datatransfer.contextsync; +import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_CONTEXT_SYNC; + import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; +import android.companion.IOnMessageReceivedListener; +import android.companion.IOnTransportsChangedListener; import android.companion.Telecom; -import android.companion.Telecom.Call; import android.content.Context; +import android.os.Binder; import android.os.UserHandle; -import android.util.Pair; import android.util.Slog; +import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; +import android.util.proto.ProtoParseException; +import android.util.proto.ProtoUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.companion.CompanionDeviceConfig; +import com.android.server.companion.transport.CompanionTransportManager; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -45,149 +50,308 @@ import java.util.Set; public class CrossDeviceSyncController { private static final String TAG = "CrossDeviceSyncController"; - private static final int BYTE_ARRAY_SIZE = 64; + + private static final int VERSION_1 = 1; + private static final int CURRENT_VERSION = VERSION_1; private final Context mContext; - private final Callback mCdmCallback; - private final Map> mUserIdToAssociationInfo = new HashMap<>(); - private final Map> mAssociationIdToStreams = - new HashMap<>(); + private final CompanionTransportManager mCompanionTransportManager; + private final List mConnectedAssociations = new ArrayList<>(); private final Set mBlocklist = new HashSet<>(); - private CallMetadataSyncCallback mInCallServiceCallMetadataSyncCallback; + private CrossDeviceSyncControllerCallback mCrossDeviceSyncControllerCallback; - public CrossDeviceSyncController(Context context, Callback callback) { + public CrossDeviceSyncController(Context context, + CompanionTransportManager companionTransportManager) { mContext = context; - mCdmCallback = callback; + mCompanionTransportManager = companionTransportManager; + mCompanionTransportManager.addListener(new IOnTransportsChangedListener.Stub() { + @Override + public void onTransportsChanged(List newAssociations) { + final long token = Binder.clearCallingIdentity(); + try { + if (!CompanionDeviceConfig.isEnabled( + CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) { + return; + } + } finally { + Binder.restoreCallingIdentity(token); + } + final List existingAssociations = new ArrayList<>( + mConnectedAssociations); + mConnectedAssociations.clear(); + mConnectedAssociations.addAll(newAssociations); + + if (mCrossDeviceSyncControllerCallback == null) { + Slog.w(TAG, "No callback to report transports changed"); + return; + } + for (AssociationInfo associationInfo : newAssociations) { + if (!existingAssociations.contains(associationInfo) + && !isAssociationBlocked(associationInfo.getId())) { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + } + } + for (AssociationInfo associationInfo : existingAssociations) { + if (!newAssociations.contains(associationInfo)) { + if (isAssociationBlocked(associationInfo.getId())) { + mBlocklist.remove(associationInfo.getId()); + } else { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + } + } + } + } + }); + mCompanionTransportManager.addListener(MESSAGE_REQUEST_CONTEXT_SYNC, + new IOnMessageReceivedListener.Stub() { + @Override + public void onMessageReceived(int associationId, byte[] data) { + if (mCrossDeviceSyncControllerCallback == null) { + Slog.w(TAG, "No callback to process context sync message"); + return; + } + mCrossDeviceSyncControllerCallback.processContextSyncMessage(associationId, + processTelecomDataFromSync(data)); + } + }); + } + + private boolean isAssociationBlocked(int associationId) { + return mBlocklist.contains(associationId); } /** Registers the call metadata callback. */ - public void registerCallMetadataSyncCallback(CallMetadataSyncCallback callback) { - mInCallServiceCallMetadataSyncCallback = callback; + public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback) { + mCrossDeviceSyncControllerCallback = callback; + for (AssociationInfo associationInfo : mConnectedAssociations) { + if (!isAssociationBlocked(associationInfo.getId())) { + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); + } + } } /** Allow specific associated devices to enable / disable syncing. */ public void setSyncEnabled(AssociationInfo associationInfo, boolean enabled) { if (enabled) { - if (mBlocklist.contains(associationInfo.getId())) { + if (isAssociationBlocked(associationInfo.getId())) { mBlocklist.remove(associationInfo.getId()); - openChannel(associationInfo); + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ true); + mCrossDeviceSyncControllerCallback.requestCrossDeviceSync(associationInfo); } } else { - if (!mBlocklist.contains(associationInfo.getId())) { + if (!isAssociationBlocked(associationInfo.getId())) { mBlocklist.add(associationInfo.getId()); - closeChannel(associationInfo); + mCrossDeviceSyncControllerCallback.updateNumberOfActiveSyncAssociations( + associationInfo.getUserId(), /* added= */ false); + // Send empty message to device to clear its data (otherwise it will get stale) + syncMessageToDevice(associationInfo.getId(), createEmptyMessage()); } } } - /** - * Opens channels to newly associated devices, and closes channels to newly disassociated - * devices. - * - * TODO(b/265466098): this needs to be limited to just connected devices - */ - public void onAssociationsChanged(int userId, List newAssociationInfoList) { - final List existingAssociationInfoList = mUserIdToAssociationInfo.get( - userId); - // Close channels to newly-disconnected devices. - for (AssociationInfo existingAssociationInfo : existingAssociationInfoList) { - if (!newAssociationInfoList.contains(existingAssociationInfo) && !mBlocklist.contains( - existingAssociationInfo.getId())) { - closeChannel(existingAssociationInfo); - } - } - // Open channels to newly-connected devices. - for (AssociationInfo newAssociationInfo : newAssociationInfoList) { - if (!existingAssociationInfoList.contains(newAssociationInfo) && !mBlocklist.contains( - newAssociationInfo.getId())) { - openChannel(newAssociationInfo); - } - } - mUserIdToAssociationInfo.put(userId, newAssociationInfoList); - } - private boolean isAdminBlocked(int userId) { return mContext.getSystemService(DevicePolicyManager.class) .getBluetoothContactSharingDisabled(UserHandle.of(userId)); } - /** Stop reading, close streams, and close secure channel. */ - private void closeChannel(AssociationInfo associationInfo) { - // TODO(b/265466098): stop reading from secure channel - final Pair streams = mAssociationIdToStreams.get( - associationInfo.getId()); - if (streams != null) { - try { - if (streams.first != null) { - streams.first.close(); - } - if (streams.second != null) { - streams.second.close(); - } - } catch (IOException e) { - Slog.e(TAG, "Could not close streams for association " + associationInfo.getId(), - e); - } - } - mCdmCallback.closeSecureChannel(associationInfo.getId()); - } - - /** Sync initial snapshot and start reading. */ - private void openChannel(AssociationInfo associationInfo) { - final InputStream is = new ByteArrayInputStream(new byte[BYTE_ARRAY_SIZE]); - final OutputStream os = new ByteArrayOutputStream(BYTE_ARRAY_SIZE); - mAssociationIdToStreams.put(associationInfo.getId(), new Pair<>(is, os)); - mCdmCallback.createSecureChannel(associationInfo.getId(), is, os); - // TODO(b/265466098): only requestSync for this specific association / connection? - mInCallServiceCallMetadataSyncCallback.requestCrossDeviceSync(associationInfo.getUserId()); - // TODO(b/265466098): start reading from secure channel - } - /** * Sync data to associated devices. * * @param userId The user whose data should be synced. * @param calls The full list of current calls for all users. */ - public void crossDeviceSync(int userId, Collection calls) { - final boolean isAdminBlocked = isAdminBlocked(userId); - for (AssociationInfo associationInfo : mUserIdToAssociationInfo.get(userId)) { - final Pair streams = mAssociationIdToStreams.get( - associationInfo.getId()); - final ProtoOutputStream pos = new ProtoOutputStream(streams.second); - final long telecomToken = pos.start(ContextSyncMessage.TELECOM); - for (CrossDeviceCall call : calls) { - final long callsToken = pos.start(Telecom.CALLS); - pos.write(Call.ID, call.getId()); - final long originToken = pos.start(Call.ORIGIN); - pos.write(Call.Origin.CALLER_ID, call.getReadableCallerId(isAdminBlocked)); - pos.write(Call.Origin.APP_ICON, call.getCallingAppIcon()); - pos.write(Call.Origin.APP_NAME, call.getCallingAppName()); - pos.end(originToken); - pos.write(Call.STATUS, call.getStatus()); - for (int control : call.getControls()) { - pos.write(Call.CONTROLS_AVAILABLE, control); - } - pos.end(callsToken); + public void syncToAllDevicesForUserId(int userId, Collection calls) { + final Set associationIds = new HashSet<>(); + for (AssociationInfo associationInfo : mConnectedAssociations) { + if (associationInfo.getUserId() == userId && !isAssociationBlocked( + associationInfo.getId())) { + associationIds.add(associationInfo.getId()); } - pos.end(telecomToken); - pos.flush(); } + if (associationIds.isEmpty()) { + Slog.w(TAG, "No eligible devices to sync to"); + return; + } + + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, + createCallUpdateMessage(calls, userId), + associationIds.stream().mapToInt(Integer::intValue).toArray()); } /** - * Callback to be implemented by CompanionDeviceManagerService. + * Sync data to associated devices. + * + * @param associationInfo The association whose data should be synced. + * @param calls The full list of current calls for all users. */ - public interface Callback { - /** - * Create a secure channel to send messages. - */ - void createSecureChannel(int associationId, InputStream input, OutputStream output); + public void syncToSingleDevice(AssociationInfo associationInfo, + Collection calls) { + if (isAssociationBlocked(associationInfo.getId())) { + Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); + return; + } - /** - * Close the secure channel created previously. - */ - void closeSecureChannel(int associationId); + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, + createCallUpdateMessage(calls, associationInfo.getUserId()), + new int[]{associationInfo.getId()}); + } + + /** + * Sync data to associated devices. + * + * @param associationId The association whose data should be synced. + * @param message The message to sync. + */ + public void syncMessageToDevice(int associationId, byte[] message) { + if (isAssociationBlocked(associationId)) { + Slog.e(TAG, "Cannot sync to requested device; connection is blocked"); + return; + } + + mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, message, + new int[]{associationId}); + } + + @VisibleForTesting + CallMetadataSyncData processTelecomDataFromSync(byte[] data) { + final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData(); + final ProtoInputStream pis = new ProtoInputStream(data); + try { + int version = -1; + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ContextSyncMessage.VERSION: + version = pis.readInt(ContextSyncMessage.VERSION); + Slog.e(TAG, "Processing context sync message version " + version); + break; + case (int) ContextSyncMessage.TELECOM: + if (version == VERSION_1) { + final long telecomToken = pis.start(ContextSyncMessage.TELECOM); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) Telecom.CALLS) { + final long callsToken = pis.start(Telecom.CALLS); + callMetadataSyncData.addCall(processCallDataFromSync(pis)); + pis.end(callsToken); + } else if (pis.getFieldNumber() == (int) Telecom.REQUESTS) { + final long requestsToken = pis.start(Telecom.REQUESTS); + callMetadataSyncData.addRequest(processCallDataFromSync(pis)); + pis.end(requestsToken); + } else { + Slog.e(TAG, "Unhandled field in Telecom:" + + ProtoUtils.currentFieldToString(pis)); + } + } + pis.end(telecomToken); + } else { + Slog.e(TAG, "Cannot process unsupported version " + version); + } + break; + default: + Slog.e(TAG, "Unhandled field in ContextSyncMessage:" + + ProtoUtils.currentFieldToString(pis)); + } + } + } catch (IOException | ProtoParseException e) { + throw new RuntimeException(e); + } + return callMetadataSyncData; + } + + @VisibleForTesting + CallMetadataSyncData.Call processCallDataFromSync(ProtoInputStream pis) throws IOException { + final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) Telecom.Call.ID: + call.setId(pis.readLong(Telecom.Call.ID)); + break; + case (int) Telecom.Call.ORIGIN: + final long originToken = pis.start(Telecom.Call.ORIGIN); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) Telecom.Call.Origin.APP_ICON: + call.setAppIcon(pis.readBytes(Telecom.Call.Origin.APP_ICON)); + break; + case (int) Telecom.Call.Origin.APP_NAME: + call.setAppName(pis.readString(Telecom.Call.Origin.APP_NAME)); + break; + case (int) Telecom.Call.Origin.CALLER_ID: + call.setCallerId(pis.readString(Telecom.Call.Origin.CALLER_ID)); + break; + case (int) Telecom.Call.Origin.APP_IDENTIFIER: + call.setAppIdentifier( + pis.readString(Telecom.Call.Origin.APP_IDENTIFIER)); + break; + default: + Slog.e(TAG, "Unhandled field in Origin:" + + ProtoUtils.currentFieldToString(pis)); + } + } + pis.end(originToken); + break; + case (int) Telecom.Call.STATUS: + call.setStatus(pis.readInt(Telecom.Call.STATUS)); + break; + case (int) Telecom.Call.CONTROLS: + call.addControl(pis.readInt(Telecom.Call.CONTROLS)); + break; + default: + Slog.e(TAG, + "Unhandled field in Telecom:" + ProtoUtils.currentFieldToString(pis)); + } + } + return call; + } + + @VisibleForTesting + byte[] createCallUpdateMessage(Collection calls, int userId) { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + final long telecomToken = pos.start(ContextSyncMessage.TELECOM); + for (CrossDeviceCall call : calls) { + final long callsToken = pos.start(Telecom.CALLS); + pos.write(Telecom.Call.ID, call.getId()); + final long originToken = pos.start(Telecom.Call.ORIGIN); + pos.write(Telecom.Call.Origin.CALLER_ID, + call.getReadableCallerId(isAdminBlocked(userId))); + pos.write(Telecom.Call.Origin.APP_ICON, call.getCallingAppIcon()); + pos.write(Telecom.Call.Origin.APP_NAME, call.getCallingAppName()); + pos.write(Telecom.Call.Origin.APP_IDENTIFIER, call.getCallingAppPackageName()); + pos.end(originToken); + pos.write(Telecom.Call.STATUS, call.getStatus()); + for (int control : call.getControls()) { + pos.write(Telecom.Call.CONTROLS, control); + } + pos.end(callsToken); + } + pos.end(telecomToken); + return pos.getBytes(); + } + + /** Create a call control message. */ + public static byte[] createCallControlMessage(long callId, int control) { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + final long telecomToken = pos.start(ContextSyncMessage.TELECOM); + final long requestsToken = pos.start(Telecom.REQUESTS); + pos.write(Telecom.Call.ID, callId); + pos.write(Telecom.Call.CONTROLS, control); + pos.end(requestsToken); + pos.end(telecomToken); + return pos.getBytes(); + } + + /** Create an empty context sync message, used to clear state. */ + public static byte[] createEmptyMessage() { + final ProtoOutputStream pos = new ProtoOutputStream(); + pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION); + return pos.getBytes(); } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncCallback.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java similarity index 67% rename from services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncCallback.java rename to services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java index 7c339d2134835..31e10a8145681 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncCallback.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerCallback.java @@ -16,12 +16,14 @@ package com.android.server.companion.datatransfer.contextsync; +import android.companion.AssociationInfo; + /** Callback for call metadata syncing. */ -public abstract class CallMetadataSyncCallback { +public abstract class CrossDeviceSyncControllerCallback { - abstract void processCallControlAction(int crossDeviceCallId, int callControlAction); + void processContextSyncMessage(int associationId, CallMetadataSyncData callMetadataSyncData) {} - abstract void requestCrossDeviceSync(int userId); + void requestCrossDeviceSync(AssociationInfo associationInfo) {} - abstract void updateStatus(int userId, boolean shouldSyncCallMetadata); + void updateNumberOfActiveSyncAssociations(int userId, boolean added) {} } diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java new file mode 100644 index 0000000000000..eec026ccfc8ad --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java @@ -0,0 +1,138 @@ +/* + * 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.contextsync; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.companion.transport.CompanionTransportManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Presubmit +@RunWith(AndroidTestingRunner.class) +public class CrossDeviceSyncControllerTest { + + private CrossDeviceSyncController mCrossDeviceSyncController; + @Mock + private CompanionTransportManager mMockCompanionTransportManager; + @Mock + private CrossDeviceCall mMockCrossDeviceCall; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mCrossDeviceSyncController = new CrossDeviceSyncController( + InstrumentationRegistry.getInstrumentation().getContext(), + mMockCompanionTransportManager); + } + + @Test + public void processTelecomDataFromSync_createCallUpdateMessage_emptyCallsAndRequests() { + final byte[] data = mCrossDeviceSyncController.createCallUpdateMessage(new HashSet<>(), + InstrumentationRegistry.getInstrumentation().getContext().getUserId()); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Unexpectedly found a call").that( + callMetadataSyncData.getCalls()).isEmpty(); + assertWithMessage("Unexpectedly found a request").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromSync_createEmptyMessage_emptyCallsAndRequests() { + final byte[] data = CrossDeviceSyncController.createEmptyMessage(); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Unexpectedly found a call").that( + callMetadataSyncData.getCalls()).isEmpty(); + assertWithMessage("Unexpectedly found a request").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromSync_createCallUpdateMessage_hasCalls() { + when(mMockCrossDeviceCall.getId()).thenReturn(5L); + final String callerId = "Firstname Lastname"; + when(mMockCrossDeviceCall.getReadableCallerId(anyBoolean())).thenReturn(callerId); + final String appName = "AppName"; + when(mMockCrossDeviceCall.getCallingAppName()).thenReturn(appName); + final String appIcon = "ABCD"; + when(mMockCrossDeviceCall.getCallingAppIcon()).thenReturn(appIcon.getBytes()); + when(mMockCrossDeviceCall.getStatus()).thenReturn(android.companion.Telecom.Call.RINGING); + final Set controls = Set.of( + android.companion.Telecom.Call.ACCEPT, + android.companion.Telecom.Call.REJECT, + android.companion.Telecom.Call.SILENCE); + when(mMockCrossDeviceCall.getControls()).thenReturn(controls); + final byte[] data = mCrossDeviceSyncController.createCallUpdateMessage( + new HashSet<>(List.of(mMockCrossDeviceCall)), + InstrumentationRegistry.getInstrumentation().getContext().getUserId()); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Wrong number of active calls").that( + callMetadataSyncData.getCalls()).hasSize(1); + final CallMetadataSyncData.Call call = + callMetadataSyncData.getCalls().stream().findAny().orElseThrow(); + assertWithMessage("Wrong id").that(call.getId()).isEqualTo(5L); + assertWithMessage("Wrong app icon").that(new String(call.getAppIcon())).isEqualTo(appIcon); + assertWithMessage("Wrong app name").that(call.getAppName()).isEqualTo(appName); + assertWithMessage("Wrong caller id").that(call.getCallerId()).isEqualTo(callerId); + assertWithMessage("Wrong status").that(call.getStatus()) + .isEqualTo(android.companion.Telecom.Call.RINGING); + assertWithMessage("Wrong controls").that(call.getControls()).isEqualTo(controls); + assertWithMessage("Unexpectedly has requests").that( + callMetadataSyncData.getRequests()).isEmpty(); + } + + @Test + public void processTelecomDataFromMessage_createCallControlMessage_hasCallControlRequest() { + final byte[] data = CrossDeviceSyncController.createCallControlMessage( + /* callId= */ 5L, /* status= */ android.companion.Telecom.Call.ACCEPT); + final CallMetadataSyncData callMetadataSyncData = + mCrossDeviceSyncController.processTelecomDataFromSync(data); + assertWithMessage("Wrong number of requests").that( + callMetadataSyncData.getRequests()).hasSize(1); + final CallMetadataSyncData.Call call = + callMetadataSyncData.getRequests().stream().findAny().orElseThrow(); + assertWithMessage("Wrong id").that(call.getId()).isEqualTo(5L); + assertWithMessage("Wrong app icon").that(call.getAppIcon()).isNull(); + assertWithMessage("Wrong app name").that(call.getAppName()).isNull(); + assertWithMessage("Wrong caller id").that(call.getCallerId()).isNull(); + assertWithMessage("Wrong status").that(call.getStatus()) + .isEqualTo(android.companion.Telecom.Call.UNKNOWN_STATUS); + assertWithMessage("Wrong controls").that(call.getControls()) + .isEqualTo(Set.of(android.companion.Telecom.Call.ACCEPT)); + assertWithMessage("Unexpectedly has active calls").that( + callMetadataSyncData.getCalls()).isEmpty(); + } +}