Merge "Split and streamline create and control requests" into udc-dev

This commit is contained in:
Christine Franks
2023-05-04 00:14:20 +00:00
committed by Android (Google) Code Review
8 changed files with 795 additions and 165 deletions

View File

@@ -31,11 +31,8 @@ message Telecom {
// Caller's name and/or phone number; what a user would see displayed when receiving an
// incoming call on the local device
string caller_id = 1;
// 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;
bytes app_icon = 2;
CallFacilitator facilitator = 3;
}
Origin origin = 2;
@@ -51,6 +48,37 @@ message Telecom {
repeated Control controls = 4;
}
message Request {
message CreateAction {
// UUID representing this request.
string id = 1;
// URI representing the address of the intended callee.
string address = 2;
// Which facilitator should handle this call.
CallFacilitator facilitator = 3;
}
message ControlAction {
// UUID representing the call to perform the control action on
string id = 1;
// The control to perform
Control control = 2;
}
oneof action {
CreateAction create_action = 1;
ControlAction control_action = 2;
}
}
// A facilitator (namely an app) that can be directed to place calls.
// Next index: 3
message CallFacilitator {
// Human-readable name of the facilitator
string name = 1;
// Unique identifier for this facilitator, such as a package name.
string identifier = 2;
}
enum Control {
UNKNOWN_CONTROL = 0;
ACCEPT = 1;
@@ -68,5 +96,7 @@ message Telecom {
// The list of active calls.
repeated Call calls = 1;
// The list of requested calls or call changes.
repeated Call requests = 2;
repeated Request requests = 2;
// The list of call facilitators that this device currently supports.
repeated CallFacilitator facilitators = 3;
}

View File

@@ -16,18 +16,21 @@
package com.android.server.companion.datatransfer.contextsync;
import android.content.ComponentName;
import android.media.AudioManager;
import android.os.Bundle;
import android.telecom.Call;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.companion.CompanionDeviceManagerServiceInternal;
import java.util.HashMap;
import java.util.Map;
@@ -39,10 +42,47 @@ public class CallMetadataSyncConnectionService extends ConnectionService {
private static final String TAG = "CallMetadataSyncConnectionService";
private AudioManager mAudioManager;
private TelecomManager mTelecomManager;
private final Map<PhoneAccountHandleIdentifier, PhoneAccountHandle> mPhoneAccountHandles =
new HashMap<>();
@VisibleForTesting
AudioManager mAudioManager;
@VisibleForTesting
TelecomManager mTelecomManager;
private CompanionDeviceManagerServiceInternal mCdmsi;
@VisibleForTesting
final Map<CallMetadataSyncConnectionIdentifier, CallMetadataSyncConnection>
mActiveConnections = new HashMap<>();
@VisibleForTesting
final CrossDeviceSyncControllerCallback
mCrossDeviceSyncControllerCallback = new CrossDeviceSyncControllerCallback() {
@Override
void processContextSyncMessage(int associationId,
CallMetadataSyncData callMetadataSyncData) {
// Add new calls or update existing calls.
for (CallMetadataSyncData.Call call : callMetadataSyncData.getCalls()) {
final CallMetadataSyncConnection existingConnection =
mActiveConnections.get(new CallMetadataSyncConnectionIdentifier(
associationId, call.getId()));
if (existingConnection == null) {
final Bundle extras = new Bundle();
extras.putInt(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID,
associationId);
extras.putParcelable(CrossDeviceSyncController.EXTRA_CALL, call);
mTelecomManager.addNewIncomingCall(call.getPhoneAccountHandle(),
extras);
} else {
existingConnection.update(call);
}
}
// Remove obsolete calls.
mActiveConnections.values().removeIf(connection -> {
if (!callMetadataSyncData.hasCall(connection.getCallId())) {
connection.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
return true;
}
return false;
});
}
};
@Override
public void onCreate() {
@@ -50,83 +90,96 @@ public class CallMetadataSyncConnectionService extends ConnectionService {
mAudioManager = getSystemService(AudioManager.class);
mTelecomManager = getSystemService(TelecomManager.class);
mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class);
mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback);
}
/**
* Registers a {@link android.telecom.PhoneAccount} for a given call-capable app on the synced
* device.
*/
private void registerPhoneAccount(int associationId, String appIdentifier,
String humanReadableAppName) {
final PhoneAccountHandleIdentifier phoneAccountHandleIdentifier =
new PhoneAccountHandleIdentifier(associationId, appIdentifier);
final PhoneAccount phoneAccount = createPhoneAccount(phoneAccountHandleIdentifier,
humanReadableAppName);
mTelecomManager.registerPhoneAccount(phoneAccount);
mTelecomManager.enablePhoneAccount(mPhoneAccountHandles.get(phoneAccountHandleIdentifier),
true);
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle phoneAccountHandle,
ConnectionRequest connectionRequest) {
final int associationId = connectionRequest.getExtras().getInt(
CrossDeviceSyncController.EXTRA_ASSOCIATION_ID);
final CallMetadataSyncData.Call call = connectionRequest.getExtras().getParcelable(
CrossDeviceSyncController.EXTRA_CALL, CallMetadataSyncData.Call.class);
final CallMetadataSyncConnection connection = new CallMetadataSyncConnection(
mTelecomManager,
mAudioManager,
associationId,
call,
new CallMetadataSyncConnectionCallback() {
@Override
void sendCallAction(int associationId, String callId, int action) {
mCdmsi.sendCrossDeviceSyncMessage(associationId,
CrossDeviceSyncController.createCallControlMessage(callId, action));
}
});
connection.setConnectionProperties(
Connection.PROPERTY_IS_EXTERNAL_CALL | Connection.PROPERTY_SELF_MANAGED);
return connection;
}
/**
* Unregisters a {@link android.telecom.PhoneAccount} for a given call-capable app on the synced
* device.
*/
private void unregisterPhoneAccount(int associationId, String appIdentifier) {
mTelecomManager.unregisterPhoneAccount(mPhoneAccountHandles.remove(
new PhoneAccountHandleIdentifier(associationId, appIdentifier)));
@Override
public void onCreateIncomingConnectionFailed(PhoneAccountHandle phoneAccountHandle,
ConnectionRequest connectionRequest) {
Slog.e(TAG, "onCreateIncomingConnectionFailed for: " + phoneAccountHandle.getId());
}
@Override
public Connection onCreateOutgoingConnection(PhoneAccountHandle phoneAccountHandle,
ConnectionRequest connectionRequest) {
final PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(phoneAccountHandle);
final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
call.setId(UUID.randomUUID().toString());
call.setStatus(android.companion.Telecom.Call.UNKNOWN_STATUS);
call.setPhoneAccountHandle(phoneAccountHandle);
final CallMetadataSyncData.CallFacilitator callFacilitator =
new CallMetadataSyncData.CallFacilitator(phoneAccount.getLabel().toString(),
phoneAccount.getExtras().getString(
CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID));
call.setFacilitator(callFacilitator);
final int associationId = connectionRequest.getExtras().getInt(
CrossDeviceSyncController.EXTRA_ASSOCIATION_ID);
final CallMetadataSyncConnection connection = new CallMetadataSyncConnection(
mTelecomManager,
mAudioManager,
associationId,
call,
new CallMetadataSyncConnectionCallback() {
@Override
void sendCallAction(int associationId, String callId, int action) {
mCdmsi.sendCrossDeviceSyncMessage(associationId,
CrossDeviceSyncController.createCallControlMessage(callId, action));
}
});
connection.setConnectionProperties(
Connection.PROPERTY_IS_EXTERNAL_CALL | Connection.PROPERTY_SELF_MANAGED);
mCdmsi.sendCrossDeviceSyncMessage(associationId,
CrossDeviceSyncController.createCallCreateMessage(call.getId(),
connectionRequest.getAddress().toString(),
call.getFacilitator().getIdentifier()));
return connection;
}
@Override
public void onCreateOutgoingConnectionFailed(PhoneAccountHandle phoneAccountHandle,
ConnectionRequest connectionRequest) {
Slog.e(TAG, "onCreateIncomingConnectionFailed for: " + phoneAccountHandle.getId());
}
@Override
public void onCreateConnectionComplete(Connection connection) {
if (connection instanceof CallMetadataSyncConnection) {
((CallMetadataSyncConnection) connection).initialize();
}
}
@VisibleForTesting
PhoneAccount createPhoneAccount(PhoneAccountHandleIdentifier phoneAccountHandleIdentifier,
String humanReadableAppName) {
if (mPhoneAccountHandles.containsKey(phoneAccountHandleIdentifier)) {
// Already exists!
return null;
}
final PhoneAccountHandle handle = new PhoneAccountHandle(
new ComponentName(this, CallMetadataSyncConnectionService.class),
UUID.randomUUID().toString());
mPhoneAccountHandles.put(phoneAccountHandleIdentifier, handle);
return new PhoneAccount.Builder(handle, humanReadableAppName)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
| PhoneAccount.CAPABILITY_SELF_MANAGED).build();
}
static final class PhoneAccountHandleIdentifier {
private final int mAssociationId;
private final String mAppIdentifier;
PhoneAccountHandleIdentifier(int associationId, String appIdentifier) {
mAssociationId = associationId;
mAppIdentifier = appIdentifier;
}
public int getAssociationId() {
return mAssociationId;
}
public String getAppIdentifier() {
return mAppIdentifier;
}
@Override
public int hashCode() {
return Objects.hash(mAssociationId, mAppIdentifier);
}
@Override
public boolean equals(Object other) {
if (other instanceof PhoneAccountHandleIdentifier) {
return ((PhoneAccountHandleIdentifier) other).getAssociationId() == mAssociationId
&& mAppIdentifier != null
&& mAppIdentifier.equals(
((PhoneAccountHandleIdentifier) other).getAppIdentifier());
}
return false;
}
}
private static final class CallMetadataSyncConnectionIdentifier {
static final class CallMetadataSyncConnectionIdentifier {
private final int mAssociationId;
private final String mCallId;
@@ -153,18 +206,21 @@ public class CallMetadataSyncConnectionService extends ConnectionService {
if (other instanceof CallMetadataSyncConnectionIdentifier) {
return ((CallMetadataSyncConnectionIdentifier) other).getAssociationId()
== mAssociationId
&& (((CallMetadataSyncConnectionIdentifier) other).getCallId() == mCallId);
&& mCallId != null && mCallId.equals(
((CallMetadataSyncConnectionIdentifier) other).getCallId());
}
return false;
}
}
private abstract static class CallMetadataSyncConnectionCallback {
@VisibleForTesting
abstract static class CallMetadataSyncConnectionCallback {
abstract void sendCallAction(int associationId, String callId, int action);
}
private static class CallMetadataSyncConnection extends Connection {
@VisibleForTesting
static class CallMetadataSyncConnection extends Connection {
private final TelecomManager mTelecomManager;
private final AudioManager mAudioManager;

View File

@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.companion.ContextSyncMessage;
import android.os.Parcel;
import android.os.Parcelable;
import android.telecom.PhoneAccountHandle;
import java.util.ArrayList;
import java.util.Collection;
@@ -34,7 +35,9 @@ import java.util.Set;
class CallMetadataSyncData {
final Map<String, CallMetadataSyncData.Call> mCalls = new HashMap<>();
final List<CallMetadataSyncData.Call> mRequests = new ArrayList<>();
final List<CallMetadataSyncData.CallCreateRequest> mCallCreateRequests = new ArrayList<>();
final List<CallMetadataSyncData.CallControlRequest> mCallControlRequests = new ArrayList<>();
final List<CallMetadataSyncData.CallFacilitator> mCallFacilitators = new ArrayList<>();
public void addCall(CallMetadataSyncData.Call call) {
mCalls.put(call.getId(), call);
@@ -48,20 +51,145 @@ class CallMetadataSyncData {
return mCalls.values();
}
public void addRequest(CallMetadataSyncData.Call call) {
mRequests.add(call);
public void addCallCreateRequest(CallMetadataSyncData.CallCreateRequest request) {
mCallCreateRequests.add(request);
}
public List<CallMetadataSyncData.Call> getRequests() {
return mRequests;
public List<CallMetadataSyncData.CallCreateRequest> getCallCreateRequests() {
return mCallCreateRequests;
}
public void addCallControlRequest(CallMetadataSyncData.CallControlRequest request) {
mCallControlRequests.add(request);
}
public List<CallMetadataSyncData.CallControlRequest> getCallControlRequests() {
return mCallControlRequests;
}
public void addFacilitator(CallFacilitator facilitator) {
mCallFacilitators.add(facilitator);
}
public List<CallFacilitator> getFacilitators() {
return mCallFacilitators;
}
public static class CallFacilitator implements Parcelable {
private String mName;
private String mIdentifier;
CallFacilitator() {}
CallFacilitator(String name, String identifier) {
mName = name;
mIdentifier = identifier;
}
CallFacilitator(Parcel parcel) {
this(parcel.readString(), parcel.readString());
}
@Override
public void writeToParcel(Parcel parcel, int parcelableFlags) {
parcel.writeString(mName);
parcel.writeString(mIdentifier);
}
public String getName() {
return mName;
}
public String getIdentifier() {
return mIdentifier;
}
public void setName(String name) {
mName = name;
}
public void setIdentifier(String identifier) {
mIdentifier = identifier;
}
@Override
public int describeContents() {
return 0;
}
@NonNull
public static final Parcelable.Creator<CallFacilitator> CREATOR =
new Parcelable.Creator<>() {
@Override
public CallFacilitator createFromParcel(Parcel source) {
return new CallFacilitator(source);
}
@Override
public CallFacilitator[] newArray(int size) {
return new CallFacilitator[size];
}
};
}
public static class CallControlRequest {
private String mId;
private int mControl;
public void setId(String id) {
mId = id;
}
public void setControl(int control) {
mControl = control;
}
public String getId() {
return mId;
}
public int getControl() {
return mControl;
}
}
public static class CallCreateRequest {
private String mId;
private String mAddress;
private CallFacilitator mFacilitator;
public void setId(String id) {
mId = id;
}
public void setAddress(String address) {
mAddress = address;
}
public void setFacilitator(CallFacilitator facilitator) {
mFacilitator = facilitator;
}
public String getId() {
return mId;
}
public String getAddress() {
return mAddress;
}
public CallFacilitator getFacilitator() {
return mFacilitator;
}
}
public static class Call implements Parcelable {
private String mId;
private String mCallerId;
private byte[] mAppIcon;
private String mAppName;
private String mAppIdentifier;
private CallFacilitator mFacilitator;
private PhoneAccountHandle mPhoneAccountHandle;
private int mStatus;
private final Set<Integer> mControls = new HashSet<>();
@@ -70,8 +198,11 @@ class CallMetadataSyncData {
call.setId(parcel.readString());
call.setCallerId(parcel.readString());
call.setAppIcon(parcel.readBlob());
call.setAppName(parcel.readString());
call.setAppIdentifier(parcel.readString());
call.setFacilitator(parcel.readParcelable(CallFacilitator.class.getClassLoader(),
CallFacilitator.class));
call.setPhoneAccountHandle(
parcel.readParcelable(PhoneAccountHandle.class.getClassLoader(),
android.telecom.PhoneAccountHandle.class));
call.setStatus(parcel.readInt());
final int numberOfControls = parcel.readInt();
for (int i = 0; i < numberOfControls; i++) {
@@ -85,8 +216,8 @@ class CallMetadataSyncData {
parcel.writeString(mId);
parcel.writeString(mCallerId);
parcel.writeBlob(mAppIcon);
parcel.writeString(mAppName);
parcel.writeString(mAppIdentifier);
parcel.writeParcelable(mFacilitator, parcelableFlags);
parcel.writeParcelable(mPhoneAccountHandle, parcelableFlags);
parcel.writeInt(mStatus);
parcel.writeInt(mControls.size());
for (int control : mControls) {
@@ -106,12 +237,12 @@ class CallMetadataSyncData {
mAppIcon = appIcon;
}
void setAppName(String appName) {
mAppName = appName;
void setFacilitator(CallFacilitator facilitator) {
mFacilitator = facilitator;
}
void setAppIdentifier(String appIdentifier) {
mAppIdentifier = appIdentifier;
void setPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) {
mPhoneAccountHandle = phoneAccountHandle;
}
void setStatus(int status) {
@@ -134,12 +265,12 @@ class CallMetadataSyncData {
return mAppIcon;
}
String getAppName() {
return mAppName;
CallFacilitator getFacilitator() {
return mFacilitator;
}
String getAppIdentifier() {
return mAppIdentifier;
PhoneAccountHandle getPhoneAccountHandle() {
return mPhoneAccountHandle;
}
int getStatus() {

View File

@@ -66,16 +66,11 @@ public class CallMetadataSyncInCallService extends InCallService {
@Override
void processContextSyncMessage(int associationId,
CallMetadataSyncData callMetadataSyncData) {
final Iterator<CallMetadataSyncData.Call> iterator =
callMetadataSyncData.getRequests().iterator();
final Iterator<CallMetadataSyncData.CallControlRequest> iterator =
callMetadataSyncData.getCallControlRequests().iterator();
while (iterator.hasNext()) {
final CallMetadataSyncData.Call call = iterator.next();
if (call.getId() != null) {
// The call is already assigned an id; treat as control invocations.
for (int control : call.getControls()) {
processCallControlAction(call.getId(), control);
}
}
final CallMetadataSyncData.CallControlRequest request = iterator.next();
processCallControlAction(request.getId(), request.getControl());
iterator.remove();
}
}

View File

@@ -24,9 +24,15 @@ import android.companion.ContextSyncMessage;
import android.companion.IOnMessageReceivedListener;
import android.companion.IOnTransportsChangedListener;
import android.companion.Telecom;
import android.content.ComponentName;
import android.content.Context;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.UserHandle;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
@@ -40,9 +46,14 @@ import com.android.server.companion.transport.CompanionTransportManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* Monitors connections and sending / receiving of synced data.
@@ -51,13 +62,25 @@ public class CrossDeviceSyncController {
private static final String TAG = "CrossDeviceSyncController";
static final String EXTRA_ASSOCIATION_ID =
"com.android.server.companion.datatransfer.contextsync.extra.ASSOCIATION_ID";
static final String EXTRA_CALL =
"com.android.server.companion.datatransfer.contextsync.extra.CALL";
static final String EXTRA_CALL_FACILITATOR_ID =
"com.android.server.companion.datatransfer.contextsync.extra.CALL_FACILITATOR_ID";
// Special facilitator id corresponding to TelecomManager#placeCall usage (with address of
// schema tel:). All other facilitators use Intent#actionCall.
public static final String FACILITATOR_ID_SYSTEM = "system";
private static final int VERSION_1 = 1;
private static final int CURRENT_VERSION = VERSION_1;
private final Context mContext;
private final CompanionTransportManager mCompanionTransportManager;
private final PhoneAccountManager mPhoneAccountManager;
private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>();
private final Set<Integer> mBlocklist = new HashSet<>();
private final List<CallMetadataSyncData.CallFacilitator> mCallFacilitators = new ArrayList<>();
private CrossDeviceSyncControllerCallback mCrossDeviceSyncControllerCallback;
@@ -81,7 +104,6 @@ public class CrossDeviceSyncController {
mConnectedAssociations);
mConnectedAssociations.clear();
mConnectedAssociations.addAll(newAssociations);
if (mCrossDeviceSyncControllerCallback == null) {
Slog.w(TAG, "No callback to report transports changed");
return;
@@ -110,14 +132,43 @@ public class CrossDeviceSyncController {
new IOnMessageReceivedListener.Stub() {
@Override
public void onMessageReceived(int associationId, byte[] data) {
final CallMetadataSyncData processedData = processTelecomDataFromSync(data);
mPhoneAccountManager.updateFacilitators(associationId, processedData);
processCallCreateRequests(associationId, processedData);
if (mCrossDeviceSyncControllerCallback == null) {
Slog.w(TAG, "No callback to process context sync message");
return;
}
mCrossDeviceSyncControllerCallback.processContextSyncMessage(associationId,
processTelecomDataFromSync(data));
processedData);
}
});
mPhoneAccountManager = new PhoneAccountManager(mContext);
}
private void processCallCreateRequests(int associationId,
CallMetadataSyncData callMetadataSyncData) {
final Iterator<CallMetadataSyncData.CallCreateRequest> iterator =
callMetadataSyncData.getCallCreateRequests().iterator();
while (iterator.hasNext()) {
final CallMetadataSyncData.CallCreateRequest request = iterator.next();
if (FACILITATOR_ID_SYSTEM.equals(request.getFacilitator().getIdentifier())) {
if (request.getAddress() != null && request.getAddress().startsWith(
PhoneAccount.SCHEME_TEL)) {
// Remove all the non-numbers (dashes, parens, scheme)
final Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL,
request.getAddress().replaceAll("\\D+", ""), /* fragment= */ null);
final Bundle extras = new Bundle();
extras.putString(CrossDeviceCall.EXTRA_CALL_ID, request.getId());
final Bundle outerExtras = new Bundle();
outerExtras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
mContext.getSystemService(TelecomManager.class).placeCall(uri, outerExtras);
}
} else {
Slog.e(TAG, "Non-system facilitated calls are not supported yet");
}
iterator.remove();
}
}
private boolean isAssociationBlocked(int associationId) {
@@ -241,8 +292,35 @@ public class CrossDeviceSyncController {
pis.end(callsToken);
} else if (pis.getFieldNumber() == (int) Telecom.REQUESTS) {
final long requestsToken = pis.start(Telecom.REQUESTS);
callMetadataSyncData.addRequest(processCallDataFromSync(pis));
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.Request.CREATE_ACTION:
final long createActionToken = pis.start(
Telecom.Request.CREATE_ACTION);
callMetadataSyncData.addCallCreateRequest(
processCallCreateRequestDataFromSync(pis));
pis.end(createActionToken);
break;
case (int) Telecom.Request.CONTROL_ACTION:
final long controlActionToken = pis.start(
Telecom.Request.CONTROL_ACTION);
callMetadataSyncData.addCallControlRequest(
processCallControlRequestDataFromSync(pis));
pis.end(controlActionToken);
break;
default:
Slog.e(TAG,
"Unhandled field in Request:"
+ ProtoUtils.currentFieldToString(
pis));
}
}
pis.end(requestsToken);
} else if (pis.getFieldNumber() == (int) Telecom.FACILITATORS) {
final long facilitatorsToken = pis.start(Telecom.FACILITATORS);
callMetadataSyncData.addFacilitator(
processFacilitatorDataFromSync(pis));
pis.end(facilitatorsToken);
} else {
Slog.e(TAG, "Unhandled field in Telecom:"
+ ProtoUtils.currentFieldToString(pis));
@@ -264,6 +342,79 @@ public class CrossDeviceSyncController {
return callMetadataSyncData;
}
/** Process an incoming message with a call create request. */
public static CallMetadataSyncData.CallCreateRequest processCallCreateRequestDataFromSync(
ProtoInputStream pis) throws IOException {
CallMetadataSyncData.CallCreateRequest callCreateRequest =
new CallMetadataSyncData.CallCreateRequest();
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.Request.CreateAction.ID:
callCreateRequest.setId(pis.readString(Telecom.Request.CreateAction.ID));
break;
case (int) Telecom.Request.CreateAction.ADDRESS:
callCreateRequest.setAddress(
pis.readString(Telecom.Request.CreateAction.ADDRESS));
break;
case (int) Telecom.Request.CreateAction.FACILITATOR:
final long facilitatorToken = pis.start(
Telecom.Request.CreateAction.FACILITATOR);
callCreateRequest.setFacilitator(processFacilitatorDataFromSync(pis));
pis.end(facilitatorToken);
break;
default:
Slog.e(TAG,
"Unhandled field in CreateAction:" + ProtoUtils.currentFieldToString(
pis));
}
}
return callCreateRequest;
}
/** Process an incoming message with a call control request. */
public static CallMetadataSyncData.CallControlRequest processCallControlRequestDataFromSync(
ProtoInputStream pis) throws IOException {
final CallMetadataSyncData.CallControlRequest callControlRequest =
new CallMetadataSyncData.CallControlRequest();
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.Request.ControlAction.ID:
callControlRequest.setId(pis.readString(Telecom.Request.ControlAction.ID));
break;
case (int) Telecom.Request.ControlAction.CONTROL:
callControlRequest.setControl(
pis.readInt(Telecom.Request.ControlAction.CONTROL));
break;
default:
Slog.e(TAG,
"Unhandled field in ControlAction:" + ProtoUtils.currentFieldToString(
pis));
}
}
return callControlRequest;
}
/** Process an incoming message with facilitators. */
public static CallMetadataSyncData.CallFacilitator processFacilitatorDataFromSync(
ProtoInputStream pis) throws IOException {
final CallMetadataSyncData.CallFacilitator facilitator =
new CallMetadataSyncData.CallFacilitator();
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.CallFacilitator.NAME:
facilitator.setName(pis.readString(Telecom.CallFacilitator.NAME));
break;
case (int) Telecom.CallFacilitator.IDENTIFIER:
facilitator.setIdentifier(pis.readString(Telecom.CallFacilitator.IDENTIFIER));
break;
default:
Slog.e(TAG, "Unhandled field in Facilitator:"
+ ProtoUtils.currentFieldToString(pis));
}
}
return facilitator;
}
@VisibleForTesting
CallMetadataSyncData.Call processCallDataFromSync(ProtoInputStream pis) throws IOException {
final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
@@ -279,15 +430,14 @@ public class CrossDeviceSyncController {
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));
case (int) Telecom.Call.Origin.FACILITATOR:
final long facilitatorToken = pis.start(
Telecom.Call.Origin.FACILITATOR);
call.setFacilitator(processFacilitatorDataFromSync(pis));
pis.end(facilitatorToken);
break;
default:
Slog.e(TAG, "Unhandled field in Origin:"
@@ -322,8 +472,10 @@ public class CrossDeviceSyncController {
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());
final long facilitatorToken = pos.start(Telecom.Call.Origin.FACILITATOR);
pos.write(Telecom.CallFacilitator.NAME, call.getCallingAppName());
pos.write(Telecom.CallFacilitator.IDENTIFIER, call.getCallingAppPackageName());
pos.end(facilitatorToken);
pos.end(originToken);
pos.write(Telecom.Call.STATUS, call.getStatus());
for (int control : call.getControls()) {
@@ -331,6 +483,12 @@ public class CrossDeviceSyncController {
}
pos.end(callsToken);
}
for (CallMetadataSyncData.CallFacilitator facilitator : mCallFacilitators) {
final long facilitatorsToken = pos.start(Telecom.FACILITATORS);
pos.write(Telecom.CallFacilitator.NAME, facilitator.getName());
pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitator.getIdentifier());
pos.end(facilitatorsToken);
}
pos.end(telecomToken);
return pos.getBytes();
}
@@ -341,8 +499,29 @@ public class CrossDeviceSyncController {
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);
final long actionToken = pos.start(Telecom.Request.CONTROL_ACTION);
pos.write(Telecom.Request.ControlAction.ID, callId);
pos.write(Telecom.Request.ControlAction.CONTROL, control);
pos.end(actionToken);
pos.end(requestsToken);
pos.end(telecomToken);
return pos.getBytes();
}
/** Create a call creation message (used to place a call). */
public static byte[] createCallCreateMessage(String id, String callAddress,
String facilitatorIdentifier) {
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);
final long actionToken = pos.start(Telecom.Request.CREATE_ACTION);
pos.write(Telecom.Request.CreateAction.ID, id);
pos.write(Telecom.Request.CreateAction.ADDRESS, callAddress);
final long facilitatorToken = pos.start(Telecom.Request.CreateAction.FACILITATOR);
pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitatorIdentifier);
pos.end(facilitatorToken);
pos.end(actionToken);
pos.end(requestsToken);
pos.end(telecomToken);
return pos.getBytes();
@@ -354,4 +533,131 @@ public class CrossDeviceSyncController {
pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
return pos.getBytes();
}
static class PhoneAccountManager {
private final Map<PhoneAccountHandleIdentifier, PhoneAccountHandle> mPhoneAccountHandles =
new HashMap<>();
private final TelecomManager mTelecomManager;
private final ComponentName mConnectionServiceComponentName;
PhoneAccountManager(Context context) {
mTelecomManager = context.getSystemService(TelecomManager.class);
mConnectionServiceComponentName = new ComponentName(context,
CallMetadataSyncConnectionService.class);
}
PhoneAccountHandle getPhoneAccountHandle(int associationId, String appIdentifier) {
return mPhoneAccountHandles.get(
new PhoneAccountHandleIdentifier(associationId, appIdentifier));
}
void updateFacilitators(int associationId, CallMetadataSyncData data) {
final ArrayList<CallMetadataSyncData.CallFacilitator> facilitators = new ArrayList<>();
for (CallMetadataSyncData.Call call : data.getCalls()) {
facilitators.add(call.getFacilitator());
}
facilitators.addAll(data.getFacilitators());
updateFacilitators(associationId, facilitators);
}
private void updateFacilitators(int associationId,
List<CallMetadataSyncData.CallFacilitator> facilitators) {
final Iterator<PhoneAccountHandleIdentifier> iterator =
mPhoneAccountHandles.keySet().iterator();
while (iterator.hasNext()) {
final PhoneAccountHandleIdentifier handleIdentifier = iterator.next();
final String handleAppIdentifier = handleIdentifier.getAppIdentifier();
final int handleAssociationId = handleIdentifier.getAssociationId();
if (associationId == handleAssociationId && facilitators.stream().noneMatch(
facilitator -> handleAppIdentifier != null && handleAppIdentifier.equals(
facilitator.getIdentifier()))) {
unregisterPhoneAccount(mPhoneAccountHandles.get(handleIdentifier));
iterator.remove();
}
}
for (CallMetadataSyncData.CallFacilitator facilitator : facilitators) {
final PhoneAccountHandleIdentifier phoneAccountHandleIdentifier =
new PhoneAccountHandleIdentifier(associationId,
facilitator.getIdentifier());
if (!mPhoneAccountHandles.containsKey(phoneAccountHandleIdentifier)) {
registerPhoneAccount(phoneAccountHandleIdentifier, facilitator.getName());
}
}
}
/**
* Registers a {@link android.telecom.PhoneAccount} for a given call-capable app on the
* synced device, and records it in the local {@link #mPhoneAccountHandles} map.
*/
private void registerPhoneAccount(PhoneAccountHandleIdentifier handleIdentifier,
String humanReadableAppName) {
if (mPhoneAccountHandles.containsKey(handleIdentifier)) {
// Already exists!
return;
}
final PhoneAccountHandle handle = new PhoneAccountHandle(
mConnectionServiceComponentName,
UUID.randomUUID().toString());
mPhoneAccountHandles.put(handleIdentifier, handle);
final PhoneAccount phoneAccount = createPhoneAccount(handle, humanReadableAppName,
handleIdentifier.getAppIdentifier());
mTelecomManager.registerPhoneAccount(phoneAccount);
mTelecomManager.enablePhoneAccount(mPhoneAccountHandles.get(handleIdentifier), true);
}
/**
* Unregisters a {@link android.telecom.PhoneAccount} for a given call-capable app on the
* synced device. Does NOT remove it from the {@link #mPhoneAccountHandles} map.
*/
private void unregisterPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
mTelecomManager.unregisterPhoneAccount(phoneAccountHandle);
}
@VisibleForTesting
static PhoneAccount createPhoneAccount(PhoneAccountHandle handle,
String humanReadableAppName,
String appIdentifier) {
final Bundle extras = new Bundle();
extras.putString(EXTRA_CALL_FACILITATOR_ID, appIdentifier);
return new PhoneAccount.Builder(handle, humanReadableAppName)
.setExtras(extras)
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
| PhoneAccount.CAPABILITY_CONNECTION_MANAGER).build();
}
}
static final class PhoneAccountHandleIdentifier {
private final int mAssociationId;
private final String mAppIdentifier;
PhoneAccountHandleIdentifier(int associationId, String appIdentifier) {
mAssociationId = associationId;
mAppIdentifier = appIdentifier;
}
public int getAssociationId() {
return mAssociationId;
}
public String getAppIdentifier() {
return mAppIdentifier;
}
@Override
public int hashCode() {
return Objects.hash(mAssociationId, mAppIdentifier);
}
@Override
public boolean equals(Object other) {
if (other instanceof PhoneAccountHandleIdentifier) {
return ((PhoneAccountHandleIdentifier) other).getAssociationId() == mAssociationId
&& mAppIdentifier != null
&& mAppIdentifier.equals(
((PhoneAccountHandleIdentifier) other).getAppIdentifier());
}
return false;
}
}
}

View File

@@ -16,52 +16,85 @@
package com.android.server.companion.datatransfer.contextsync;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.media.AudioManager;
import android.platform.test.annotations.Presubmit;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.testing.AndroidTestingRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Presubmit
@RunWith(AndroidTestingRunner.class)
public class CallMetadataSyncConnectionServiceTest {
private CallMetadataSyncConnectionService mSyncConnectionService;
@Mock
private TelecomManager mMockTelecomManager;
@Mock
private AudioManager mMockAudioManager;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
doNothing().when(mMockTelecomManager).registerPhoneAccount(any());
doNothing().when(mMockTelecomManager).unregisterPhoneAccount(any());
mSyncConnectionService = new CallMetadataSyncConnectionService() {
@Override
public String getPackageName() {
return "android";
}
};
mSyncConnectionService.mTelecomManager = mMockTelecomManager;
mSyncConnectionService.mAudioManager = mMockAudioManager;
}
@Test
public void createPhoneAccount_success() {
final PhoneAccount phoneAccount = mSyncConnectionService.createPhoneAccount(
new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
associationId= */
0, "com.google.test"), "Test App");
assertWithMessage("Could not create phone account").that(phoneAccount).isNotNull();
public void processContextSyncMessage_empty() {
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
mSyncConnectionService.mCrossDeviceSyncControllerCallback.processContextSyncMessage(
/* associationId= */ 0, callMetadataSyncData);
verify(mMockTelecomManager, never()).addNewIncomingCall(any(), any());
}
@Test
public void createPhoneAccount_alreadyExists_doesNotCreateAnother() {
final PhoneAccount phoneAccount = mSyncConnectionService.createPhoneAccount(
new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
associationId= */
0, "com.google.test"), "Test App");
final PhoneAccount phoneAccount2 = mSyncConnectionService.createPhoneAccount(
new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
associationId= */
0, "com.google.test"), "Test App #2");
assertWithMessage("Could not create phone account").that(phoneAccount).isNotNull();
assertWithMessage("Unexpectedly created second phone account").that(phoneAccount2).isNull();
public void processContextSyncMessage_newCall() {
final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
call.setId("123abc");
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
callMetadataSyncData.addCall(call);
mSyncConnectionService.mCrossDeviceSyncControllerCallback.processContextSyncMessage(
/* associationId= */ 0, callMetadataSyncData);
verify(mMockTelecomManager, times(1)).addNewIncomingCall(any(), any());
}
@Test
public void processContextSyncMessage_existingCall() {
final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
call.setId("123abc");
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
callMetadataSyncData.addCall(call);
mSyncConnectionService.mActiveConnections.put(
new CallMetadataSyncConnectionService.CallMetadataSyncConnectionIdentifier(
/* asscociationId= */ 0, "123abc"),
new CallMetadataSyncConnectionService.CallMetadataSyncConnection(
mMockTelecomManager, mMockAudioManager, 0, call,
new CallMetadataSyncConnectionService.CallMetadataSyncConnectionCallback() {
@Override
void sendCallAction(int associationId, String callId, int action) {}
}));
mSyncConnectionService.mCrossDeviceSyncControllerCallback.processContextSyncMessage(
/* associationId= */ 0, callMetadataSyncData);
verify(mMockTelecomManager, never()).addNewIncomingCall(any(), any());
}
}

View File

@@ -41,8 +41,9 @@ public class CallMetadataSyncDataTest {
call.setId(id);
call.setCallerId(callerId);
call.setAppIcon(appIcon);
call.setAppName(appName);
call.setAppIdentifier(appIdentifier);
final CallMetadataSyncData.CallFacilitator callFacilitator =
new CallMetadataSyncData.CallFacilitator(appName, appIdentifier);
call.setFacilitator(callFacilitator);
call.setStatus(status);
call.addControl(control1);
call.addControl(control2);
@@ -56,8 +57,8 @@ public class CallMetadataSyncDataTest {
assertThat(reconstructedCall.getId()).isEqualTo(id);
assertThat(reconstructedCall.getCallerId()).isEqualTo(callerId);
assertThat(reconstructedCall.getAppIcon()).isEqualTo(appIcon);
assertThat(reconstructedCall.getAppName()).isEqualTo(appName);
assertThat(reconstructedCall.getAppIdentifier()).isEqualTo(appIdentifier);
assertThat(reconstructedCall.getFacilitator().getName()).isEqualTo(appName);
assertThat(reconstructedCall.getFacilitator().getIdentifier()).isEqualTo(appIdentifier);
assertThat(reconstructedCall.getStatus()).isEqualTo(status);
assertThat(reconstructedCall.getControls()).containsExactly(control1, control2);
}

View File

@@ -18,10 +18,19 @@ package com.android.server.companion.datatransfer.contextsync;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Context;
import android.platform.test.annotations.Presubmit;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.testing.AndroidTestingRunner;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -47,6 +56,10 @@ public class CrossDeviceSyncControllerTest {
private CompanionTransportManager mMockCompanionTransportManager;
@Mock
private CrossDeviceCall mMockCrossDeviceCall;
@Mock
private TelecomManager mMockTelecomManager;
@Mock
private Context mMockContext;
@Before
public void setUp() throws Exception {
@@ -54,6 +67,12 @@ public class CrossDeviceSyncControllerTest {
mCrossDeviceSyncController = new CrossDeviceSyncController(
InstrumentationRegistry.getInstrumentation().getContext(),
mMockCompanionTransportManager);
when(mMockContext.getSystemServiceName(TelecomManager.class))
.thenReturn(Context.TELECOM_SERVICE);
when(mMockContext.getSystemService(Context.TELECOM_SERVICE))
.thenReturn(mMockTelecomManager);
doNothing().when(mMockTelecomManager).registerPhoneAccount(any());
doNothing().when(mMockTelecomManager).unregisterPhoneAccount(any());
}
@Test
@@ -64,8 +83,10 @@ public class CrossDeviceSyncControllerTest {
mCrossDeviceSyncController.processTelecomDataFromSync(data);
assertWithMessage("Unexpectedly found a call").that(
callMetadataSyncData.getCalls()).isEmpty();
assertWithMessage("Unexpectedly found a request").that(
callMetadataSyncData.getRequests()).isEmpty();
assertWithMessage("Unexpectedly has control requests").that(
callMetadataSyncData.getCallControlRequests()).isEmpty();
assertWithMessage("Unexpectedly has create requests").that(
callMetadataSyncData.getCallCreateRequests()).isEmpty();
}
@Test
@@ -75,8 +96,10 @@ public class CrossDeviceSyncControllerTest {
mCrossDeviceSyncController.processTelecomDataFromSync(data);
assertWithMessage("Unexpectedly found a call").that(
callMetadataSyncData.getCalls()).isEmpty();
assertWithMessage("Unexpectedly found a request").that(
callMetadataSyncData.getRequests()).isEmpty();
assertWithMessage("Unexpectedly has control requests").that(
callMetadataSyncData.getCallControlRequests()).isEmpty();
assertWithMessage("Unexpectedly has create requests").that(
callMetadataSyncData.getCallCreateRequests()).isEmpty();
}
@Test
@@ -86,6 +109,8 @@ public class CrossDeviceSyncControllerTest {
when(mMockCrossDeviceCall.getReadableCallerId(anyBoolean())).thenReturn(callerId);
final String appName = "AppName";
when(mMockCrossDeviceCall.getCallingAppName()).thenReturn(appName);
final String pkgName = "com.google.test";
when(mMockCrossDeviceCall.getCallingAppPackageName()).thenReturn(pkgName);
final String appIcon = "ABCD";
when(mMockCrossDeviceCall.getCallingAppIcon()).thenReturn(appIcon.getBytes());
when(mMockCrossDeviceCall.getStatus()).thenReturn(android.companion.Telecom.Call.RINGING);
@@ -105,32 +130,85 @@ public class CrossDeviceSyncControllerTest {
callMetadataSyncData.getCalls().stream().findAny().orElseThrow();
assertWithMessage("Wrong id").that(call.getId()).isEqualTo("123abc");
assertWithMessage("Wrong app icon").that(new String(call.getAppIcon())).isEqualTo(appIcon);
assertWithMessage("Wrong app name").that(call.getAppName()).isEqualTo(appName);
final CallMetadataSyncData.CallFacilitator facilitator = call.getFacilitator();
assertWithMessage("Wrong app name").that(facilitator.getName()).isEqualTo(appName);
assertWithMessage("Wrong pkg name").that(facilitator.getIdentifier()).isEqualTo(pkgName);
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 control requests").that(
callMetadataSyncData.getCallControlRequests()).isEmpty();
assertWithMessage("Unexpectedly has create requests").that(
callMetadataSyncData.getCallCreateRequests()).isEmpty();
}
@Test
public void processTelecomDataFromMessage_createCallControlMessage_hasCallControlRequest() {
final byte[] data = CrossDeviceSyncController.createCallControlMessage(
/* callId= */ "123abc", /* status= */ android.companion.Telecom.ACCEPT);
/* callId= */ "5678abc", /* status= */ android.companion.Telecom.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("123abc");
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.ACCEPT));
callMetadataSyncData.getCallControlRequests()).hasSize(1);
final CallMetadataSyncData.CallControlRequest request =
callMetadataSyncData.getCallControlRequests().stream().findAny().orElseThrow();
assertWithMessage("Wrong id").that(request.getId()).isEqualTo("5678abc");
assertWithMessage("Wrong control").that(request.getControl())
.isEqualTo(android.companion.Telecom.ACCEPT);
assertWithMessage("Unexpectedly has active calls").that(
callMetadataSyncData.getCalls()).isEmpty();
}
@Test
public void createPhoneAccount_success() {
final PhoneAccount phoneAccount =
CrossDeviceSyncController.PhoneAccountManager.createPhoneAccount(
new PhoneAccountHandle(
new ComponentName("com.google.test", "com.google.test.Activity"),
"id"), "Test App", "com.google.test");
assertWithMessage("Could not create phone account").that(phoneAccount).isNotNull();
}
@Test
public void updateFacilitators_alreadyExists_doesNotCreateAnother() {
final CrossDeviceSyncController.PhoneAccountManager phoneAccountManager =
new CrossDeviceSyncController.PhoneAccountManager(mMockContext);
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
callMetadataSyncData.addFacilitator(
new CallMetadataSyncData.CallFacilitator("name", "com.google.test"));
phoneAccountManager.updateFacilitators(0, callMetadataSyncData);
phoneAccountManager.updateFacilitators(0, callMetadataSyncData);
verify(mMockTelecomManager, times(1)).registerPhoneAccount(any());
verify(mMockTelecomManager, times(0)).unregisterPhoneAccount(any());
}
@Test
public void updateFacilitators_new_addsIt() {
final CrossDeviceSyncController.PhoneAccountManager phoneAccountManager =
new CrossDeviceSyncController.PhoneAccountManager(mMockContext);
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
callMetadataSyncData.addFacilitator(
new CallMetadataSyncData.CallFacilitator("name", "com.google.test"));
phoneAccountManager.updateFacilitators(0, callMetadataSyncData);
callMetadataSyncData.addFacilitator(
new CallMetadataSyncData.CallFacilitator("name", "com.google.test2"));
phoneAccountManager.updateFacilitators(0, callMetadataSyncData);
verify(mMockTelecomManager, times(2)).registerPhoneAccount(any());
verify(mMockTelecomManager, times(0)).unregisterPhoneAccount(any());
}
@Test
public void updateFacilitators_old_removesIt() {
final CrossDeviceSyncController.PhoneAccountManager phoneAccountManager =
new CrossDeviceSyncController.PhoneAccountManager(mMockContext);
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
callMetadataSyncData.addFacilitator(
new CallMetadataSyncData.CallFacilitator("name", "com.google.test"));
phoneAccountManager.updateFacilitators(0, callMetadataSyncData);
final CallMetadataSyncData callMetadataSyncData2 = new CallMetadataSyncData();
phoneAccountManager.updateFacilitators(0, callMetadataSyncData2);
verify(mMockTelecomManager, times(1)).registerPhoneAccount(any());
verify(mMockTelecomManager, times(1)).unregisterPhoneAccount(any());
}
}