Send / recv contextsync messages via securechannel
Bug: 265466098 Test: atest FrameworksServicesTests:com.android.server.companion.datatransfer Change-Id: I3e17e44b47b9592c3fe7d8f18fa3ad1dda16d786
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user