diff --git a/core/proto/android/companion/telecom.proto b/core/proto/android/companion/telecom.proto index 02ba7c5e39ffc..b90067dae2a26 100644 --- a/core/proto/android/companion/telecom.proto +++ b/core/proto/android/companion/telecom.proto @@ -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; } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java index e436e9300fb57..459bf989321a1 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java @@ -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 mPhoneAccountHandles = - new HashMap<>(); + @VisibleForTesting + AudioManager mAudioManager; + @VisibleForTesting + TelecomManager mTelecomManager; + private CompanionDeviceManagerServiceInternal mCdmsi; + @VisibleForTesting + final Map + 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; diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java index 5b0c745a7173a..b3cf772fc470b 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java @@ -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 mCalls = new HashMap<>(); - final List mRequests = new ArrayList<>(); + final List mCallCreateRequests = new ArrayList<>(); + final List mCallControlRequests = new ArrayList<>(); + final List 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 getRequests() { - return mRequests; + public List getCallCreateRequests() { + return mCallCreateRequests; + } + + public void addCallControlRequest(CallMetadataSyncData.CallControlRequest request) { + mCallControlRequests.add(request); + } + + public List getCallControlRequests() { + return mCallControlRequests; + } + + public void addFacilitator(CallFacilitator facilitator) { + mCallFacilitators.add(facilitator); + } + + public List 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 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 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() { diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java index 0c23730231771..1f5e168b14e4a 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java @@ -66,16 +66,11 @@ public class CallMetadataSyncInCallService extends InCallService { @Override void processContextSyncMessage(int associationId, CallMetadataSyncData callMetadataSyncData) { - final Iterator iterator = - callMetadataSyncData.getRequests().iterator(); + final Iterator 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(); } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java index e5ab963b6cbb5..937d7fed35429 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java +++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncController.java @@ -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 mConnectedAssociations = new ArrayList<>(); private final Set mBlocklist = new HashSet<>(); + private final List 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 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 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 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 facilitators) { + final Iterator 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; + } + } } diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java index bacf2568d9ca9..ccddb2fd95bc7 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java @@ -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()); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java index dcd06c9cc716f..8a107ada16b0f 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java @@ -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); } diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java index 25b0ae4862304..33e7cd2891fc6 100644 --- a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CrossDeviceSyncControllerTest.java @@ -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()); + } }