Send / recv contextsync messages via securechannel

Bug: 265466098
Test: atest FrameworksServicesTests:com.android.server.companion.datatransfer
Change-Id: I3e17e44b47b9592c3fe7d8f18fa3ad1dda16d786
This commit is contained in:
Christine Franks
2023-04-25 13:53:26 -07:00
parent 6522287338
commit f64304aa6d
7 changed files with 625 additions and 186 deletions

View File

@@ -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;
}

View File

@@ -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<IOnAssociationsChangedListener> 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<CrossDeviceCall> calls) {
if (CompanionDeviceConfig.isEnabled(
CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
mCrossDeviceSyncController.syncToAllDevicesForUserId(userId, calls);
}
}
@Override
public void crossDeviceSync(AssociationInfo associationInfo,
Collection<CrossDeviceCall> 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);
}
}
}
/**

View File

@@ -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<CrossDeviceCall> calls);
/**
* Requests a sync from an InCallService to CDM, for the given association and call metadata.
*/
void crossDeviceSync(AssociationInfo associationInfo, Collection<CrossDeviceCall> calls);
}

View File

@@ -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<Call, CrossDeviceCall> 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<CallMetadataSyncData.Call> 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());
}
}

View File

@@ -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<Integer, List<AssociationInfo>> mUserIdToAssociationInfo = new HashMap<>();
private final Map<Integer, Pair<InputStream, OutputStream>> mAssociationIdToStreams =
new HashMap<>();
private final CompanionTransportManager mCompanionTransportManager;
private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>();
private final Set<Integer> 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<AssociationInfo> newAssociations) {
final long token = Binder.clearCallingIdentity();
try {
if (!CompanionDeviceConfig.isEnabled(
CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
return;
}
} finally {
Binder.restoreCallingIdentity(token);
}
final List<AssociationInfo> 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<AssociationInfo> newAssociationInfoList) {
final List<AssociationInfo> 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<InputStream, OutputStream> 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<CrossDeviceCall> calls) {
final boolean isAdminBlocked = isAdminBlocked(userId);
for (AssociationInfo associationInfo : mUserIdToAssociationInfo.get(userId)) {
final Pair<InputStream, OutputStream> 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<CrossDeviceCall> calls) {
final Set<Integer> 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<CrossDeviceCall> 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<CrossDeviceCall> 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();
}
}

View File

@@ -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) {}
}

View File

@@ -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<Integer> 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();
}
}