blob: bf82f3f4a352d48e3490fc49e014ed5b2bd60ab8 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.companion.datatransfer.contextsync;
import static com.android.server.companion.transport.Transport.MESSAGE_REQUEST_CONTEXT_SYNC;
import android.app.admin.DevicePolicyManager;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
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;
import android.util.proto.ProtoParseException;
import android.util.proto.ProtoUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.companion.CompanionDeviceConfig;
import com.android.server.companion.transport.CompanionTransportManager;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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;
import java.util.stream.Collectors;
/**
* Monitors connections and sending / receiving of synced data.
*/
public class CrossDeviceSyncController {
private static final String TAG = "CrossDeviceSyncController";
public static final String EXTRA_CALL_ID =
"com.android.companion.datatransfer.contextsync.extra.CALL_ID";
static final String EXTRA_FACILITATOR_ICON =
"com.android.companion.datatransfer.contextsync.extra.FACILITATOR_ICON";
static final String EXTRA_IS_REMOTE_ORIGIN =
"com.android.companion.datatransfer.contextsync.extra.IS_REMOTE_ORIGIN";
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 CallManager mCallManager;
private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>();
private final Set<Integer> mBlocklist = new HashSet<>();
private final List<CallMetadataSyncData.CallFacilitator> mCallFacilitators = new ArrayList<>();
private WeakReference<CrossDeviceSyncControllerCallback> mInCallServiceCallbackRef;
private WeakReference<CrossDeviceSyncControllerCallback> mConnectionServiceCallbackRef;
public CrossDeviceSyncController(Context context,
CompanionTransportManager companionTransportManager) {
mContext = context;
mCompanionTransportManager = companionTransportManager;
mCompanionTransportManager.addListener(new IOnTransportsChangedListener.Stub() {
@Override
public void onTransportsChanged(List<AssociationInfo> newAssociations) {
final long token = Binder.clearCallingIdentity();
try {
if (!CompanionDeviceConfig.isEnabled(
CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
return;
}
} finally {
Binder.restoreCallingIdentity(token);
}
final List<AssociationInfo> existingAssociations = new ArrayList<>(
mConnectedAssociations);
mConnectedAssociations.clear();
mConnectedAssociations.addAll(newAssociations);
for (AssociationInfo associationInfo : newAssociations) {
if (!existingAssociations.contains(associationInfo)) {
// New association.
if (!isAssociationBlocked(associationInfo)) {
final CrossDeviceSyncControllerCallback callback =
mInCallServiceCallbackRef != null
? mInCallServiceCallbackRef.get() : null;
if (callback != null) {
callback.updateNumberOfActiveSyncAssociations(
associationInfo.getUserId(), /* added= */ true);
callback.requestCrossDeviceSync(associationInfo);
} else {
Slog.w(TAG, "No callback to report new transport");
syncMessageToDevice(associationInfo.getId(),
createFacilitatorMessage());
}
} else {
mBlocklist.add(associationInfo.getId());
Slog.i(TAG, "New association was blocked from context syncing");
}
}
}
for (AssociationInfo associationInfo : existingAssociations) {
if (!newAssociations.contains(associationInfo)) {
// Removed association!
mBlocklist.remove(associationInfo.getId());
if (!isAssociationBlockedLocal(associationInfo.getId())) {
final CrossDeviceSyncControllerCallback callback =
mInCallServiceCallbackRef != null
? mInCallServiceCallbackRef.get() : null;
if (callback != null) {
callback.updateNumberOfActiveSyncAssociations(
associationInfo.getUserId(), /* added= */ false);
} else {
Slog.w(TAG, "No callback to report removed transport");
}
}
} else {
// Stable association!
final boolean systemBlocked = isAssociationBlocked(associationInfo);
if (isAssociationBlockedLocal(associationInfo.getId()) != systemBlocked) {
// Block state has changed.
final CrossDeviceSyncControllerCallback callback =
mInCallServiceCallbackRef != null
? mInCallServiceCallbackRef.get() : null;
if (!systemBlocked) {
Slog.i(TAG, "Unblocking existing association for context sync");
mBlocklist.remove(associationInfo.getId());
if (callback != null) {
callback.updateNumberOfActiveSyncAssociations(
associationInfo.getUserId(), /* added= */ true);
callback.requestCrossDeviceSync(associationInfo);
} else {
Slog.w(TAG, "No callback to report changed transport");
syncMessageToDevice(associationInfo.getId(),
createFacilitatorMessage());
}
} else {
Slog.i(TAG, "Blocking existing association for context sync");
mBlocklist.add(associationInfo.getId());
if (callback != null) {
callback.updateNumberOfActiveSyncAssociations(
associationInfo.getUserId(), /* added= */ false);
} else {
Slog.w(TAG, "No callback to report changed transport");
}
// Send empty message to device to clear its data (otherwise it
// will get stale)
syncMessageToDevice(associationInfo.getId(),
createEmptyMessage());
}
}
}
}
}
});
mCompanionTransportManager.addListener(MESSAGE_REQUEST_CONTEXT_SYNC,
new IOnMessageReceivedListener.Stub() {
@Override
public void onMessageReceived(int associationId, byte[] data) {
if (isAssociationBlockedLocal(associationId)) {
return;
}
final CallMetadataSyncData processedData = processTelecomDataFromSync(data);
mPhoneAccountManager.updateFacilitators(associationId, processedData);
mCallManager.updateCalls(associationId, processedData);
processCallCreateRequests(processedData);
if (mInCallServiceCallbackRef == null
&& mConnectionServiceCallbackRef == null) {
Slog.w(TAG, "No callback to process context sync message");
return;
}
final CrossDeviceSyncControllerCallback inCallServiceCallback =
mInCallServiceCallbackRef != null ? mInCallServiceCallbackRef.get()
: null;
if (inCallServiceCallback != null) {
inCallServiceCallback.processContextSyncMessage(associationId,
processedData);
} else {
// This is dead; get rid of it lazily
mInCallServiceCallbackRef = null;
}
final CrossDeviceSyncControllerCallback connectionServiceCallback =
mConnectionServiceCallbackRef != null
? mConnectionServiceCallbackRef.get() : null;
if (connectionServiceCallback != null) {
connectionServiceCallback.processContextSyncMessage(associationId,
processedData);
} else {
// This is dead; get rid of it lazily
mConnectionServiceCallbackRef = null;
}
}
});
mPhoneAccountManager = new PhoneAccountManager(mContext);
mCallManager = new CallManager(mContext, mPhoneAccountManager);
}
private static boolean isAssociationBlocked(AssociationInfo info) {
return (info.getSystemDataSyncFlags() & CompanionDeviceManager.FLAG_CALL_METADATA)
!= CompanionDeviceManager.FLAG_CALL_METADATA;
}
/** Invoke set-up tasks that happen when boot is completed. */
public void onBootCompleted() {
if (!CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
return;
}
mPhoneAccountManager.onBootCompleted();
final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
if (telecomManager != null && telecomManager.getCallCapablePhoneAccounts().size() != 0) {
final PhoneAccountHandle defaultOutgoingTelAccountHandle =
telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
if (defaultOutgoingTelAccountHandle != null) {
final PhoneAccount defaultOutgoingTelAccount = telecomManager.getPhoneAccount(
defaultOutgoingTelAccountHandle);
if (defaultOutgoingTelAccount != null) {
mCallFacilitators.add(
new CallMetadataSyncData.CallFacilitator(
defaultOutgoingTelAccount.getLabel().toString(),
FACILITATOR_ID_SYSTEM));
}
}
}
}
private void processCallCreateRequests(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(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();
}
}
/**
* This keeps track of "previous" state to calculate deltas. Use {@link #isAssociationBlocked}
* for all other use cases.
*/
private boolean isAssociationBlockedLocal(int associationId) {
return mBlocklist.contains(associationId);
}
/** Registers the call metadata callback. */
public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback,
@CrossDeviceSyncControllerCallback.Type int type) {
if (type == CrossDeviceSyncControllerCallback.TYPE_IN_CALL_SERVICE) {
mInCallServiceCallbackRef = new WeakReference<>(callback);
for (AssociationInfo associationInfo : mConnectedAssociations) {
if (!isAssociationBlocked(associationInfo)) {
mBlocklist.remove(associationInfo.getId());
callback.updateNumberOfActiveSyncAssociations(associationInfo.getUserId(),
/* added= */ true);
callback.requestCrossDeviceSync(associationInfo);
} else {
mBlocklist.add(associationInfo.getId());
}
}
} else if (type == CrossDeviceSyncControllerCallback.TYPE_CONNECTION_SERVICE) {
mConnectionServiceCallbackRef = new WeakReference<>(callback);
} else {
Slog.e(TAG, "Cannot register callback of unknown type: " + type);
}
}
private boolean isAdminBlocked(int userId) {
return mContext.getSystemService(DevicePolicyManager.class)
.getBluetoothContactSharingDisabled(UserHandle.of(userId));
}
/**
* Sync data to associated devices.
*
* @param userId The user whose data should be synced.
* @param calls The full list of current calls for all users.
*/
public void syncToAllDevicesForUserId(int userId, Collection<CrossDeviceCall> calls) {
final Set<Integer> associationIds = new HashSet<>();
for (AssociationInfo associationInfo : mConnectedAssociations) {
if (associationInfo.getUserId() == userId && !isAssociationBlocked(associationInfo)) {
associationIds.add(associationInfo.getId());
}
}
if (associationIds.isEmpty()) {
Slog.w(TAG, "No eligible devices to sync to");
return;
}
mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC,
createCallUpdateMessage(calls, userId),
associationIds.stream().mapToInt(Integer::intValue).toArray());
}
/**
* Sync data to associated devices.
*
* @param associationInfo The association whose data should be synced.
* @param calls The full list of current calls for all users.
*/
public void syncToSingleDevice(AssociationInfo associationInfo,
Collection<CrossDeviceCall> calls) {
if (isAssociationBlocked(associationInfo)) {
Slog.e(TAG, "Cannot sync to requested device; connection is blocked");
return;
}
mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC,
createCallUpdateMessage(calls, associationInfo.getUserId()),
new int[]{associationInfo.getId()});
}
/**
* Sync data to associated devices.
*
* @param associationId The association whose data should be synced.
* @param message The message to sync.
*/
public void syncMessageToDevice(int associationId, byte[] message) {
if (isAssociationBlockedLocal(associationId)) {
Slog.e(TAG, "Cannot sync to requested device; connection is blocked");
return;
}
mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, message,
new int[]{associationId});
}
@VisibleForTesting
CallMetadataSyncData processTelecomDataFromSync(byte[] data) {
final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
final ProtoInputStream pis = new ProtoInputStream(data);
try {
int version = -1;
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) ContextSyncMessage.VERSION:
version = pis.readInt(ContextSyncMessage.VERSION);
Slog.e(TAG, "Processing context sync message version " + version);
break;
case (int) ContextSyncMessage.TELECOM:
if (version == VERSION_1) {
final long telecomToken = pis.start(ContextSyncMessage.TELECOM);
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (pis.getFieldNumber() == (int) Telecom.CALLS) {
final long callsToken = pis.start(Telecom.CALLS);
callMetadataSyncData.addCall(processCallDataFromSync(pis));
pis.end(callsToken);
} else if (pis.getFieldNumber() == (int) Telecom.REQUESTS) {
final long requestsToken = pis.start(Telecom.REQUESTS);
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));
}
}
pis.end(telecomToken);
} else {
Slog.e(TAG, "Cannot process unsupported version " + version);
}
break;
default:
Slog.e(TAG, "Unhandled field in ContextSyncMessage:"
+ ProtoUtils.currentFieldToString(pis));
}
}
} catch (IOException | ProtoParseException e) {
throw new RuntimeException(e);
}
return callMetadataSyncData;
}
/** 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();
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.Call.ID:
call.setId(pis.readString(Telecom.Call.ID));
break;
case (int) Telecom.Call.ORIGIN:
final long originToken = pis.start(Telecom.Call.ORIGIN);
while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (pis.getFieldNumber()) {
case (int) Telecom.Call.Origin.APP_ICON:
call.setAppIcon(pis.readBytes(Telecom.Call.Origin.APP_ICON));
break;
case (int) Telecom.Call.Origin.CALLER_ID:
call.setCallerId(pis.readString(Telecom.Call.Origin.CALLER_ID));
break;
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:"
+ ProtoUtils.currentFieldToString(pis));
}
}
pis.end(originToken);
break;
case (int) Telecom.Call.STATUS:
call.setStatus(pis.readInt(Telecom.Call.STATUS));
break;
case (int) Telecom.Call.CONTROLS:
call.addControl(pis.readInt(Telecom.Call.CONTROLS));
break;
default:
Slog.e(TAG,
"Unhandled field in Telecom:" + ProtoUtils.currentFieldToString(pis));
}
}
return call;
}
@VisibleForTesting
byte[] createCallUpdateMessage(Collection<CrossDeviceCall> calls, int userId) {
final ProtoOutputStream pos = new ProtoOutputStream();
pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
for (CrossDeviceCall call : calls) {
if (call.isCallPlacedByContextSync()) {
// Do not sync any calls which our "ours" as that would be duplicative.
continue;
}
final long callsToken = pos.start(Telecom.CALLS);
pos.write(Telecom.Call.ID, call.getId());
final long originToken = pos.start(Telecom.Call.ORIGIN);
pos.write(Telecom.Call.Origin.CALLER_ID,
call.getReadableCallerId(isAdminBlocked(userId)));
pos.write(Telecom.Call.Origin.APP_ICON, call.getCallingAppIcon());
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()) {
pos.write(Telecom.Call.CONTROLS, control);
}
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();
}
/** Create a call control message. */
public static byte[] createCallControlMessage(String callId, int control) {
final ProtoOutputStream pos = new ProtoOutputStream();
pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
final long requestsToken = pos.start(Telecom.REQUESTS);
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();
}
/** Create an empty context sync message, used to clear state. */
public static byte[] createEmptyMessage() {
final ProtoOutputStream pos = new ProtoOutputStream();
pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
return pos.getBytes();
}
/** Create a facilitator-only message, used before any calls are available as a call intake. */
private byte[] createFacilitatorMessage() {
return createCallUpdateMessage(Collections.emptyList(), -1);
}
@VisibleForTesting
static class CallManager {
@VisibleForTesting final Map<Integer, Set<String>> mCallIds = new HashMap<>();
private final TelecomManager mTelecomManager;
private final PhoneAccountManager mPhoneAccountManager;
CallManager(Context context, PhoneAccountManager phoneAccountManager) {
mTelecomManager = context.getSystemService(TelecomManager.class);
mPhoneAccountManager = phoneAccountManager;
}
/** Add any new calls to Telecom. The ConnectionService will handle everything else. */
void updateCalls(int associationId, CallMetadataSyncData data) {
final Set<String> oldCallIds = mCallIds.getOrDefault(associationId, new HashSet<>());
final Set<String> newCallIds = data.getCalls().stream().map(
CallMetadataSyncData.Call::getId).collect(Collectors.toSet());
if (oldCallIds.equals(newCallIds)) {
return;
}
for (CallMetadataSyncData.Call currentCall : data.getCalls()) {
if (!oldCallIds.contains(currentCall.getId())
&& currentCall.getFacilitator() != null) {
final Bundle extras = new Bundle();
extras.putInt(EXTRA_ASSOCIATION_ID, associationId);
extras.putBoolean(EXTRA_IS_REMOTE_ORIGIN, true);
extras.putParcelable(EXTRA_CALL, currentCall);
extras.putString(EXTRA_CALL_ID, currentCall.getId());
extras.putByteArray(EXTRA_FACILITATOR_ICON, currentCall.getAppIcon());
final PhoneAccountHandle handle = mPhoneAccountManager.getPhoneAccountHandle(
associationId, currentCall.getFacilitator().getIdentifier());
mTelecomManager.addNewIncomingCall(handle, extras);
}
}
mCallIds.put(associationId, newCallIds);
}
}
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);
}
void onBootCompleted() {
mTelecomManager.clearPhoneAccounts();
}
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;
}
}
}