1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.companion.datatransfer.contextsync;
18 
19 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC;
20 
21 import android.app.admin.DevicePolicyManager;
22 import android.companion.AssociationInfo;
23 import android.companion.CompanionDeviceManager;
24 import android.companion.ContextSyncMessage;
25 import android.companion.IOnMessageReceivedListener;
26 import android.companion.IOnTransportsChangedListener;
27 import android.companion.Telecom;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.net.Uri;
31 import android.os.Binder;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.telecom.PhoneAccount;
35 import android.telecom.PhoneAccountHandle;
36 import android.telecom.TelecomManager;
37 import android.util.Slog;
38 import android.util.proto.ProtoInputStream;
39 import android.util.proto.ProtoOutputStream;
40 import android.util.proto.ProtoParseException;
41 import android.util.proto.ProtoUtils;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.server.companion.CompanionDeviceConfig;
45 import com.android.server.companion.transport.CompanionTransportManager;
46 
47 import java.io.IOException;
48 import java.lang.ref.WeakReference;
49 import java.util.ArrayList;
50 import java.util.Collection;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.Iterator;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.UUID;
60 import java.util.stream.Collectors;
61 
62 /**
63  * Monitors connections and sending / receiving of synced data.
64  */
65 public class CrossDeviceSyncController {
66 
67     private static final String TAG = "CrossDeviceSyncController";
68 
69     public static final String EXTRA_CALL_ID =
70             "com.android.companion.datatransfer.contextsync.extra.CALL_ID";
71     static final String EXTRA_FACILITATOR_ICON =
72             "com.android.companion.datatransfer.contextsync.extra.FACILITATOR_ICON";
73     static final String EXTRA_IS_REMOTE_ORIGIN =
74             "com.android.companion.datatransfer.contextsync.extra.IS_REMOTE_ORIGIN";
75 
76     static final String EXTRA_ASSOCIATION_ID =
77             "com.android.server.companion.datatransfer.contextsync.extra.ASSOCIATION_ID";
78     static final String EXTRA_CALL =
79             "com.android.server.companion.datatransfer.contextsync.extra.CALL";
80     static final String EXTRA_CALL_FACILITATOR_ID =
81             "com.android.server.companion.datatransfer.contextsync.extra.CALL_FACILITATOR_ID";
82     // Special facilitator id corresponding to TelecomManager#placeCall usage (with address of
83     // schema tel:). All other facilitators use Intent#actionCall.
84     public static final String FACILITATOR_ID_SYSTEM = "system";
85 
86     private static final int VERSION_1 = 1;
87     private static final int CURRENT_VERSION = VERSION_1;
88 
89     private final Context mContext;
90     private final CompanionTransportManager mCompanionTransportManager;
91     private final PhoneAccountManager mPhoneAccountManager;
92     private final CallManager mCallManager;
93     private final List<AssociationInfo> mConnectedAssociations = new ArrayList<>();
94     private final Set<Integer> mBlocklist = new HashSet<>();
95     private final List<CallMetadataSyncData.CallFacilitator> mCallFacilitators = new ArrayList<>();
96 
97     private WeakReference<CrossDeviceSyncControllerCallback> mInCallServiceCallbackRef;
98     private WeakReference<CrossDeviceSyncControllerCallback> mConnectionServiceCallbackRef;
99 
CrossDeviceSyncController(Context context, CompanionTransportManager companionTransportManager)100     public CrossDeviceSyncController(Context context,
101             CompanionTransportManager companionTransportManager) {
102         mContext = context;
103         mCompanionTransportManager = companionTransportManager;
104         mCompanionTransportManager.addListener(new IOnTransportsChangedListener.Stub() {
105             @Override
106             public void onTransportsChanged(List<AssociationInfo> newAssociations) {
107                 final long token = Binder.clearCallingIdentity();
108                 try {
109                     if (!CompanionDeviceConfig.isEnabled(
110                             CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
111                         return;
112                     }
113                 } finally {
114                     Binder.restoreCallingIdentity(token);
115                 }
116                 final List<AssociationInfo> existingAssociations = new ArrayList<>(
117                         mConnectedAssociations);
118                 mConnectedAssociations.clear();
119                 mConnectedAssociations.addAll(newAssociations);
120                 for (AssociationInfo associationInfo : newAssociations) {
121                     if (!existingAssociations.contains(associationInfo)) {
122                         // New association.
123                         if (!isAssociationBlocked(associationInfo)) {
124                             final CrossDeviceSyncControllerCallback callback =
125                                     mInCallServiceCallbackRef != null
126                                             ? mInCallServiceCallbackRef.get() : null;
127                             if (callback != null) {
128                                 callback.updateNumberOfActiveSyncAssociations(
129                                         associationInfo.getUserId(), /* added= */ true);
130                                 callback.requestCrossDeviceSync(associationInfo);
131                             } else {
132                                 Slog.w(TAG, "No callback to report new transport");
133                                 syncMessageToDevice(associationInfo.getId(),
134                                         createFacilitatorMessage());
135                             }
136                         } else {
137                             mBlocklist.add(associationInfo.getId());
138                             Slog.i(TAG, "New association was blocked from context syncing");
139                         }
140                     }
141                 }
142                 for (AssociationInfo associationInfo : existingAssociations) {
143                     if (!newAssociations.contains(associationInfo)) {
144                         // Removed association!
145                         mBlocklist.remove(associationInfo.getId());
146                         if (!isAssociationBlockedLocal(associationInfo.getId())) {
147                             final CrossDeviceSyncControllerCallback callback =
148                                     mInCallServiceCallbackRef != null
149                                             ? mInCallServiceCallbackRef.get() : null;
150                             if (callback != null) {
151                                 callback.updateNumberOfActiveSyncAssociations(
152                                         associationInfo.getUserId(), /* added= */ false);
153                             } else {
154                                 Slog.w(TAG, "No callback to report removed transport");
155                             }
156                         }
157                         clearInProgressCalls(associationInfo.getId());
158                     } else {
159                         // Stable association!
160                         final boolean systemBlocked = isAssociationBlocked(associationInfo);
161                         if (isAssociationBlockedLocal(associationInfo.getId()) != systemBlocked) {
162                             // Block state has changed.
163                             final CrossDeviceSyncControllerCallback callback =
164                                     mInCallServiceCallbackRef != null
165                                             ? mInCallServiceCallbackRef.get() : null;
166                             if (!systemBlocked) {
167                                 Slog.i(TAG, "Unblocking existing association for context sync");
168                                 mBlocklist.remove(associationInfo.getId());
169                                 if (callback != null) {
170                                     callback.updateNumberOfActiveSyncAssociations(
171                                             associationInfo.getUserId(), /* added= */ true);
172                                     callback.requestCrossDeviceSync(associationInfo);
173                                 } else {
174                                     Slog.w(TAG, "No callback to report changed transport");
175                                     syncMessageToDevice(associationInfo.getId(),
176                                             createFacilitatorMessage());
177                                 }
178                             } else {
179                                 Slog.i(TAG, "Blocking existing association for context sync");
180                                 mBlocklist.add(associationInfo.getId());
181                                 if (callback != null) {
182                                     callback.updateNumberOfActiveSyncAssociations(
183                                             associationInfo.getUserId(), /* added= */ false);
184                                 } else {
185                                     Slog.w(TAG, "No callback to report changed transport");
186                                 }
187                                 // Send empty message to device to clear its data (otherwise it
188                                 // will get stale)
189                                 syncMessageToDevice(associationInfo.getId(),
190                                         createEmptyMessage());
191                                 clearInProgressCalls(associationInfo.getId());
192                             }
193                         }
194                     }
195                 }
196             }
197         });
198         mCompanionTransportManager.addListener(MESSAGE_REQUEST_CONTEXT_SYNC,
199                 new IOnMessageReceivedListener.Stub() {
200                     @Override
201                     public void onMessageReceived(int associationId, byte[] data) {
202                         if (isAssociationBlockedLocal(associationId)) {
203                             return;
204                         }
205                         final CallMetadataSyncData processedData = processTelecomDataFromSync(data);
206                         final boolean isRequest = processedData.getCallControlRequests().size() != 0
207                                 || processedData.getCallCreateRequests().size() != 0;
208                         if (!isRequest) {
209                             mPhoneAccountManager.updateFacilitators(associationId, processedData);
210                             mCallManager.updateCalls(associationId, processedData);
211                         } else {
212                             processCallCreateRequests(processedData);
213                         }
214                         if (mInCallServiceCallbackRef == null
215                                 && mConnectionServiceCallbackRef == null) {
216                             Slog.w(TAG, "No callback to process context sync message");
217                             return;
218                         }
219                         final CrossDeviceSyncControllerCallback inCallServiceCallback =
220                                 mInCallServiceCallbackRef != null ? mInCallServiceCallbackRef.get()
221                                         : null;
222                         if (inCallServiceCallback != null) {
223                             if (isRequest) {
224                                 inCallServiceCallback.processContextSyncMessage(associationId,
225                                         processedData);
226                             }
227                         } else {
228                             // This is dead; get rid of it lazily
229                             mInCallServiceCallbackRef = null;
230                         }
231 
232                         final CrossDeviceSyncControllerCallback connectionServiceCallback =
233                                 mConnectionServiceCallbackRef != null
234                                         ? mConnectionServiceCallbackRef.get() : null;
235                         if (connectionServiceCallback != null) {
236                             if (!isRequest) {
237                                 connectionServiceCallback.processContextSyncMessage(associationId,
238                                         processedData);
239                             }
240                         } else {
241                             // This is dead; get rid of it lazily
242                             mConnectionServiceCallbackRef = null;
243                         }
244                     }
245                 });
246         mPhoneAccountManager = new PhoneAccountManager(mContext);
247         mCallManager = new CallManager(mContext, mPhoneAccountManager);
248     }
249 
clearInProgressCalls(int associationId)250     private void clearInProgressCalls(int associationId) {
251         final Set<String> removedIds = mCallManager.clearCallIdsForAssociationId(associationId);
252         final CrossDeviceSyncControllerCallback connectionServiceCallback =
253                 mConnectionServiceCallbackRef != null ? mConnectionServiceCallbackRef.get() : null;
254         if (connectionServiceCallback != null) {
255             connectionServiceCallback.cleanUpCallIds(removedIds);
256         }
257     }
258 
isAssociationBlocked(AssociationInfo info)259     private static boolean isAssociationBlocked(AssociationInfo info) {
260         return (info.getSystemDataSyncFlags() & CompanionDeviceManager.FLAG_CALL_METADATA)
261                 != CompanionDeviceManager.FLAG_CALL_METADATA;
262     }
263 
264     /** Invoke set-up tasks that happen when boot is completed. */
onBootCompleted()265     public void onBootCompleted() {
266         if (!CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
267             return;
268         }
269 
270         mPhoneAccountManager.onBootCompleted();
271 
272         final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
273         if (telecomManager != null && telecomManager.getCallCapablePhoneAccounts().size() != 0) {
274             final PhoneAccountHandle defaultOutgoingTelAccountHandle =
275                     telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
276             if (defaultOutgoingTelAccountHandle != null) {
277                 final PhoneAccount defaultOutgoingTelAccount = telecomManager.getPhoneAccount(
278                         defaultOutgoingTelAccountHandle);
279                 if (defaultOutgoingTelAccount != null) {
280                     mCallFacilitators.add(
281                             new CallMetadataSyncData.CallFacilitator(
282                                     defaultOutgoingTelAccount.getLabel().toString(),
283                                     FACILITATOR_ID_SYSTEM, FACILITATOR_ID_SYSTEM));
284                 }
285             }
286         }
287     }
288 
processCallCreateRequests(CallMetadataSyncData callMetadataSyncData)289     private void processCallCreateRequests(CallMetadataSyncData callMetadataSyncData) {
290         final Iterator<CallMetadataSyncData.CallCreateRequest> iterator =
291                 callMetadataSyncData.getCallCreateRequests().iterator();
292         while (iterator.hasNext()) {
293             final CallMetadataSyncData.CallCreateRequest request = iterator.next();
294             if (FACILITATOR_ID_SYSTEM.equals(request.getFacilitator().getIdentifier())) {
295                 if (request.getAddress() != null && request.getAddress().startsWith(
296                         PhoneAccount.SCHEME_TEL)) {
297                     mCallManager.addSelfOwnedCallId(request.getId());
298                     // Remove all the non-numbers (dashes, parens, scheme)
299                     final Uri uri = Uri.fromParts(PhoneAccount.SCHEME_TEL,
300                             request.getAddress().replaceAll("\\D+", ""), /* fragment= */ null);
301                     final Bundle extras = new Bundle();
302                     extras.putString(EXTRA_CALL_ID, request.getId());
303                     final Bundle outerExtras = new Bundle();
304                     outerExtras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
305                     mContext.getSystemService(TelecomManager.class).placeCall(uri, outerExtras);
306                 }
307             } else {
308                 Slog.e(TAG, "Non-system facilitated calls are not supported yet");
309             }
310             iterator.remove();
311         }
312     }
313 
314     /**
315      * This keeps track of "previous" state to calculate deltas. Use {@link #isAssociationBlocked}
316      * for all other use cases.
317      */
isAssociationBlockedLocal(int associationId)318     private boolean isAssociationBlockedLocal(int associationId) {
319         return mBlocklist.contains(associationId);
320     }
321 
322     /** Registers the call metadata callback. */
registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, @CrossDeviceSyncControllerCallback.Type int type)323     public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback,
324             @CrossDeviceSyncControllerCallback.Type int type) {
325         if (type == CrossDeviceSyncControllerCallback.TYPE_IN_CALL_SERVICE) {
326             mInCallServiceCallbackRef = new WeakReference<>(callback);
327             for (AssociationInfo associationInfo : mConnectedAssociations) {
328                 if (!isAssociationBlocked(associationInfo)) {
329                     mBlocklist.remove(associationInfo.getId());
330                     callback.updateNumberOfActiveSyncAssociations(associationInfo.getUserId(),
331                             /* added= */ true);
332                     callback.requestCrossDeviceSync(associationInfo);
333                 } else {
334                     mBlocklist.add(associationInfo.getId());
335                 }
336             }
337         } else if (type == CrossDeviceSyncControllerCallback.TYPE_CONNECTION_SERVICE) {
338             mConnectionServiceCallbackRef = new WeakReference<>(callback);
339         } else {
340             Slog.e(TAG, "Cannot register callback of unknown type: " + type);
341         }
342     }
343 
isAdminBlocked(int userId)344     private boolean isAdminBlocked(int userId) {
345         return mContext.getSystemService(DevicePolicyManager.class)
346                 .getBluetoothContactSharingDisabled(UserHandle.of(userId));
347     }
348 
349     /**
350      * Sync data to associated devices.
351      *
352      * @param userId The user whose data should be synced.
353      * @param calls The full list of current calls for all users.
354      */
syncToAllDevicesForUserId(int userId, Collection<CrossDeviceCall> calls)355     public void syncToAllDevicesForUserId(int userId, Collection<CrossDeviceCall> calls) {
356         final Set<Integer> associationIds = new HashSet<>();
357         for (AssociationInfo associationInfo : mConnectedAssociations) {
358             if (associationInfo.getUserId() == userId && !isAssociationBlocked(associationInfo)) {
359                 associationIds.add(associationInfo.getId());
360             }
361         }
362         if (associationIds.isEmpty()) {
363             Slog.w(TAG, "No eligible devices to sync to");
364             return;
365         }
366 
367         mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC,
368                 createCallUpdateMessage(calls, userId),
369                 associationIds.stream().mapToInt(Integer::intValue).toArray());
370     }
371 
372     /**
373      * Sync data to associated devices.
374      *
375      * @param associationInfo The association whose data should be synced.
376      * @param calls           The full list of current calls for all users.
377      */
syncToSingleDevice(AssociationInfo associationInfo, Collection<CrossDeviceCall> calls)378     public void syncToSingleDevice(AssociationInfo associationInfo,
379             Collection<CrossDeviceCall> calls) {
380         if (isAssociationBlocked(associationInfo)) {
381             Slog.e(TAG, "Cannot sync to requested device; connection is blocked");
382             return;
383         }
384 
385         mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC,
386                 createCallUpdateMessage(calls, associationInfo.getUserId()),
387                 new int[]{associationInfo.getId()});
388     }
389 
390     /**
391      * Sync data to associated devices.
392      *
393      * @param associationId   The association whose data should be synced.
394      * @param message         The message to sync.
395      */
syncMessageToDevice(int associationId, byte[] message)396     public void syncMessageToDevice(int associationId, byte[] message) {
397         if (isAssociationBlockedLocal(associationId)) {
398             Slog.e(TAG, "Cannot sync to requested device; connection is blocked");
399             return;
400         }
401 
402         mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, message,
403                 new int[]{associationId});
404     }
405 
406     /** Sync message to all associated devices. */
syncMessageToAllDevicesForUserId(int userId, byte[] message)407     public void syncMessageToAllDevicesForUserId(int userId, byte[] message) {
408         final Set<Integer> associationIds = new HashSet<>();
409         for (AssociationInfo associationInfo : mConnectedAssociations) {
410             if (associationInfo.getUserId() == userId && !isAssociationBlocked(associationInfo)) {
411                 associationIds.add(associationInfo.getId());
412             }
413         }
414         if (associationIds.isEmpty()) {
415             Slog.w(TAG, "No eligible devices to sync to");
416             return;
417         }
418 
419         mCompanionTransportManager.sendMessage(MESSAGE_REQUEST_CONTEXT_SYNC, message,
420                 associationIds.stream().mapToInt(Integer::intValue).toArray());
421     }
422 
423     /**
424      * Mark a call id as owned (i.e. this device owns the canonical call). Note that both sides will
425      * own outgoing calls that were placed on behalf of another device.
426      */
addSelfOwnedCallId(String callId)427     public void addSelfOwnedCallId(String callId) {
428         mCallManager.addSelfOwnedCallId(callId);
429     }
430 
431     /** Unmark a call id as owned (i.e. this device no longer owns the canonical call). */
removeSelfOwnedCallId(String callId)432     public void removeSelfOwnedCallId(String callId) {
433         if (callId != null) {
434             mCallManager.removeSelfOwnedCallId(callId);
435         }
436     }
437 
438     @VisibleForTesting
processTelecomDataFromSync(byte[] data)439     CallMetadataSyncData processTelecomDataFromSync(byte[] data) {
440         final CallMetadataSyncData callMetadataSyncData = new CallMetadataSyncData();
441         final ProtoInputStream pis = new ProtoInputStream(data);
442         try {
443             int version = -1;
444             while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
445                 switch (pis.getFieldNumber()) {
446                     case (int) ContextSyncMessage.VERSION:
447                         version = pis.readInt(ContextSyncMessage.VERSION);
448                         Slog.e(TAG, "Processing context sync message version " + version);
449                         break;
450                     case (int) ContextSyncMessage.TELECOM:
451                         if (version == VERSION_1) {
452                             final long telecomToken = pis.start(ContextSyncMessage.TELECOM);
453                             while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
454                                 if (pis.getFieldNumber() == (int) Telecom.CALLS) {
455                                     final long callsToken = pis.start(Telecom.CALLS);
456                                     callMetadataSyncData.addCall(processCallDataFromSync(pis));
457                                     pis.end(callsToken);
458                                 } else if (pis.getFieldNumber() == (int) Telecom.REQUESTS) {
459                                     final long requestsToken = pis.start(Telecom.REQUESTS);
460                                     while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
461                                         switch (pis.getFieldNumber()) {
462                                             case (int) Telecom.Request.CREATE_ACTION:
463                                                 final long createActionToken = pis.start(
464                                                         Telecom.Request.CREATE_ACTION);
465                                                 callMetadataSyncData.addCallCreateRequest(
466                                                         processCallCreateRequestDataFromSync(pis));
467                                                 pis.end(createActionToken);
468                                                 break;
469                                             case (int) Telecom.Request.CONTROL_ACTION:
470                                                 final long controlActionToken = pis.start(
471                                                         Telecom.Request.CONTROL_ACTION);
472                                                 callMetadataSyncData.addCallControlRequest(
473                                                         processCallControlRequestDataFromSync(pis));
474                                                 pis.end(controlActionToken);
475                                                 break;
476                                             default:
477                                                 Slog.e(TAG,
478                                                         "Unhandled field in Request:"
479                                                                 + ProtoUtils.currentFieldToString(
480                                                                 pis));
481                                         }
482                                     }
483                                     pis.end(requestsToken);
484                                 } else if (pis.getFieldNumber() == (int) Telecom.FACILITATORS) {
485                                     final long facilitatorsToken = pis.start(Telecom.FACILITATORS);
486                                     final CallMetadataSyncData.CallFacilitator facilitator =
487                                             processFacilitatorDataFromSync(pis);
488                                     facilitator.setIsTel(true);
489                                     callMetadataSyncData.addFacilitator(facilitator);
490                                     pis.end(facilitatorsToken);
491                                 } else {
492                                     Slog.e(TAG, "Unhandled field in Telecom:"
493                                             + ProtoUtils.currentFieldToString(pis));
494                                 }
495                             }
496                             pis.end(telecomToken);
497                         } else {
498                             Slog.e(TAG, "Cannot process unsupported version " + version);
499                         }
500                         break;
501                     default:
502                         Slog.e(TAG, "Unhandled field in ContextSyncMessage:"
503                                 + ProtoUtils.currentFieldToString(pis));
504                 }
505             }
506         } catch (IOException | ProtoParseException e) {
507             throw new RuntimeException(e);
508         }
509         return callMetadataSyncData;
510     }
511 
512     /** Process an incoming message with a call create request. */
processCallCreateRequestDataFromSync( ProtoInputStream pis)513     public static CallMetadataSyncData.CallCreateRequest processCallCreateRequestDataFromSync(
514             ProtoInputStream pis) throws IOException {
515         CallMetadataSyncData.CallCreateRequest callCreateRequest =
516                 new CallMetadataSyncData.CallCreateRequest();
517         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
518             switch (pis.getFieldNumber()) {
519                 case (int) Telecom.Request.CreateAction.ID:
520                     callCreateRequest.setId(pis.readString(Telecom.Request.CreateAction.ID));
521                     break;
522                 case (int) Telecom.Request.CreateAction.ADDRESS:
523                     callCreateRequest.setAddress(
524                             pis.readString(Telecom.Request.CreateAction.ADDRESS));
525                     break;
526                 case (int) Telecom.Request.CreateAction.FACILITATOR:
527                     final long facilitatorToken = pis.start(
528                             Telecom.Request.CreateAction.FACILITATOR);
529                     callCreateRequest.setFacilitator(processFacilitatorDataFromSync(pis));
530                     pis.end(facilitatorToken);
531                     break;
532                 default:
533                     Slog.e(TAG,
534                             "Unhandled field in CreateAction:" + ProtoUtils.currentFieldToString(
535                                     pis));
536             }
537         }
538         return callCreateRequest;
539     }
540 
541     /** Process an incoming message with a call control request. */
processCallControlRequestDataFromSync( ProtoInputStream pis)542     public static CallMetadataSyncData.CallControlRequest processCallControlRequestDataFromSync(
543             ProtoInputStream pis) throws IOException {
544         final CallMetadataSyncData.CallControlRequest callControlRequest =
545                 new CallMetadataSyncData.CallControlRequest();
546         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
547             switch (pis.getFieldNumber()) {
548                 case (int) Telecom.Request.ControlAction.ID:
549                     callControlRequest.setId(pis.readString(Telecom.Request.ControlAction.ID));
550                     break;
551                 case (int) Telecom.Request.ControlAction.CONTROL:
552                     callControlRequest.setControl(
553                             pis.readInt(Telecom.Request.ControlAction.CONTROL));
554                     break;
555                 default:
556                     Slog.e(TAG,
557                             "Unhandled field in ControlAction:" + ProtoUtils.currentFieldToString(
558                                     pis));
559             }
560         }
561         return callControlRequest;
562     }
563 
564     /** Process an incoming message with facilitators. */
processFacilitatorDataFromSync( ProtoInputStream pis)565     public static CallMetadataSyncData.CallFacilitator processFacilitatorDataFromSync(
566             ProtoInputStream pis) throws IOException {
567         final CallMetadataSyncData.CallFacilitator facilitator =
568                 new CallMetadataSyncData.CallFacilitator();
569         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
570             switch (pis.getFieldNumber()) {
571                 case (int) Telecom.CallFacilitator.NAME:
572                     facilitator.setName(pis.readString(Telecom.CallFacilitator.NAME));
573                     break;
574                 case (int) Telecom.CallFacilitator.IDENTIFIER:
575                     facilitator.setIdentifier(pis.readString(Telecom.CallFacilitator.IDENTIFIER));
576                     break;
577                 case (int) Telecom.CallFacilitator.EXTENDED_IDENTIFIER:
578                     facilitator.setExtendedIdentifier(
579                             pis.readString(Telecom.CallFacilitator.EXTENDED_IDENTIFIER));
580                     break;
581                 default:
582                     Slog.e(TAG, "Unhandled field in Facilitator:"
583                             + ProtoUtils.currentFieldToString(pis));
584             }
585         }
586         return facilitator;
587     }
588 
589     @VisibleForTesting
processCallDataFromSync(ProtoInputStream pis)590     CallMetadataSyncData.Call processCallDataFromSync(ProtoInputStream pis) throws IOException {
591         final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
592         while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
593             switch (pis.getFieldNumber()) {
594                 case (int) Telecom.Call.ID:
595                     call.setId(pis.readString(Telecom.Call.ID));
596                     break;
597                 case (int) Telecom.Call.ORIGIN:
598                     final long originToken = pis.start(Telecom.Call.ORIGIN);
599                     while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
600                         switch (pis.getFieldNumber()) {
601                             case (int) Telecom.Call.Origin.APP_ICON:
602                                 call.setAppIcon(pis.readBytes(Telecom.Call.Origin.APP_ICON));
603                                 break;
604                             case (int) Telecom.Call.Origin.CALLER_ID:
605                                 call.setCallerId(pis.readString(Telecom.Call.Origin.CALLER_ID));
606                                 break;
607                             case (int) Telecom.Call.Origin.FACILITATOR:
608                                 final long facilitatorToken = pis.start(
609                                         Telecom.Call.Origin.FACILITATOR);
610                                 call.setFacilitator(processFacilitatorDataFromSync(pis));
611                                 pis.end(facilitatorToken);
612                                 break;
613                             default:
614                                 Slog.e(TAG, "Unhandled field in Origin:"
615                                         + ProtoUtils.currentFieldToString(pis));
616                         }
617                     }
618                     pis.end(originToken);
619                     break;
620                 case (int) Telecom.Call.STATUS:
621                     call.setStatus(pis.readInt(Telecom.Call.STATUS));
622                     break;
623                 case (int) Telecom.Call.DIRECTION:
624                     call.setDirection(pis.readInt(Telecom.Call.DIRECTION));
625                     break;
626                 case (int) Telecom.Call.CONTROLS:
627                     call.addControl(pis.readInt(Telecom.Call.CONTROLS));
628                     break;
629                 default:
630                     Slog.e(TAG,
631                             "Unhandled field in Telecom:" + ProtoUtils.currentFieldToString(pis));
632             }
633         }
634         return call;
635     }
636 
637     @VisibleForTesting
createCallUpdateMessage(Collection<CrossDeviceCall> calls, int userId)638     byte[] createCallUpdateMessage(Collection<CrossDeviceCall> calls, int userId) {
639         final ProtoOutputStream pos = new ProtoOutputStream();
640         pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
641         final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
642         for (CrossDeviceCall call : calls) {
643             if (call.isCallPlacedByContextSync() || mCallManager.isExternallyOwned(call.getId())) {
644                 // Do not sync any of "our" calls, nor external calls, as that would be duplicative.
645                 continue;
646             }
647             final long callsToken = pos.start(Telecom.CALLS);
648             pos.write(Telecom.Call.ID, call.getId());
649             final long originToken = pos.start(Telecom.Call.ORIGIN);
650             pos.write(Telecom.Call.Origin.CALLER_ID,
651                     call.getReadableCallerId(isAdminBlocked(call.getUserId())));
652             pos.write(Telecom.Call.Origin.APP_ICON, call.getCallingAppIcon());
653             final long facilitatorToken = pos.start(Telecom.Call.Origin.FACILITATOR);
654             pos.write(Telecom.CallFacilitator.NAME, call.getCallingAppName());
655             pos.write(Telecom.CallFacilitator.IDENTIFIER, call.getCallingAppPackageName());
656             pos.write(Telecom.CallFacilitator.EXTENDED_IDENTIFIER,
657                     call.getSerializedPhoneAccountHandle());
658             pos.end(facilitatorToken);
659             pos.end(originToken);
660             pos.write(Telecom.Call.STATUS, call.getStatus());
661             pos.write(Telecom.Call.DIRECTION, call.getDirection());
662             for (int control : call.getControls()) {
663                 pos.write(Telecom.Call.CONTROLS, control);
664             }
665             pos.end(callsToken);
666         }
667         for (CallMetadataSyncData.CallFacilitator facilitator : mCallFacilitators) {
668             final long facilitatorsToken = pos.start(Telecom.FACILITATORS);
669             pos.write(Telecom.CallFacilitator.NAME, facilitator.getName());
670             pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitator.getIdentifier());
671             pos.write(Telecom.CallFacilitator.EXTENDED_IDENTIFIER,
672                     facilitator.getExtendedIdentifier());
673             pos.end(facilitatorsToken);
674         }
675         pos.end(telecomToken);
676         return pos.getBytes();
677     }
678 
679     /** Create a call control message. */
createCallControlMessage(String callId, int control)680     public static byte[] createCallControlMessage(String callId, int control) {
681         final ProtoOutputStream pos = new ProtoOutputStream();
682         pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
683         final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
684         final long requestsToken = pos.start(Telecom.REQUESTS);
685         final long actionToken = pos.start(Telecom.Request.CONTROL_ACTION);
686         pos.write(Telecom.Request.ControlAction.ID, callId);
687         pos.write(Telecom.Request.ControlAction.CONTROL, control);
688         pos.end(actionToken);
689         pos.end(requestsToken);
690         pos.end(telecomToken);
691         return pos.getBytes();
692     }
693 
694     /** Create a call creation message (used to place a call). */
createCallCreateMessage(String id, String callAddress, String facilitatorIdentifier)695     public static byte[] createCallCreateMessage(String id, String callAddress,
696             String facilitatorIdentifier) {
697         final ProtoOutputStream pos = new ProtoOutputStream();
698         pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
699         final long telecomToken = pos.start(ContextSyncMessage.TELECOM);
700         final long requestsToken = pos.start(Telecom.REQUESTS);
701         final long actionToken = pos.start(Telecom.Request.CREATE_ACTION);
702         pos.write(Telecom.Request.CreateAction.ID, id);
703         pos.write(Telecom.Request.CreateAction.ADDRESS, callAddress);
704         final long facilitatorToken = pos.start(Telecom.Request.CreateAction.FACILITATOR);
705         pos.write(Telecom.CallFacilitator.IDENTIFIER, facilitatorIdentifier);
706         pos.end(facilitatorToken);
707         pos.end(actionToken);
708         pos.end(requestsToken);
709         pos.end(telecomToken);
710         return pos.getBytes();
711     }
712 
713     /** Create an empty context sync message, used to clear state. */
createEmptyMessage()714     public static byte[] createEmptyMessage() {
715         final ProtoOutputStream pos = new ProtoOutputStream();
716         pos.write(ContextSyncMessage.VERSION, CURRENT_VERSION);
717         return pos.getBytes();
718     }
719 
720     /** Create a facilitator-only message, used before any calls are available as a call intake. */
createFacilitatorMessage()721     private byte[] createFacilitatorMessage() {
722         return createCallUpdateMessage(Collections.emptyList(), -1);
723     }
724 
725     @VisibleForTesting
726     static class CallManager {
727 
728         @VisibleForTesting final Set<String> mSelfOwnedCalls = new HashSet<>();
729         @VisibleForTesting final Set<String> mExternallyOwnedCalls = new HashSet<>();
730 
731         @VisibleForTesting final Map<Integer, Set<String>> mCallIds = new HashMap<>();
732         private final TelecomManager mTelecomManager;
733         private final PhoneAccountManager mPhoneAccountManager;
734 
CallManager(Context context, PhoneAccountManager phoneAccountManager)735         CallManager(Context context, PhoneAccountManager phoneAccountManager) {
736             mTelecomManager = context.getSystemService(TelecomManager.class);
737             mPhoneAccountManager = phoneAccountManager;
738         }
739 
740         /** Add any new calls to Telecom. The ConnectionService will handle everything else. */
updateCalls(int associationId, CallMetadataSyncData data)741         void updateCalls(int associationId, CallMetadataSyncData data) {
742             final Set<String> oldCallIds = mCallIds.getOrDefault(associationId, new HashSet<>());
743             final Set<String> newCallIds = data.getCalls().stream().map(
744                     CallMetadataSyncData.Call::getId).collect(Collectors.toSet());
745             if (oldCallIds.equals(newCallIds)) {
746                 return;
747             }
748 
749             for (CallMetadataSyncData.Call currentCall : data.getCalls()) {
750                 if (!oldCallIds.contains(currentCall.getId())
751                         && currentCall.getFacilitator() != null
752                         && !isSelfOwned(currentCall.getId())) {
753                     mExternallyOwnedCalls.add(currentCall.getId());
754                     final Bundle extras = new Bundle();
755                     extras.putInt(EXTRA_ASSOCIATION_ID, associationId);
756                     extras.putBoolean(EXTRA_IS_REMOTE_ORIGIN, true);
757                     extras.putBundle(EXTRA_CALL, currentCall.writeToBundle());
758                     extras.putString(EXTRA_CALL_ID, currentCall.getId());
759                     extras.putByteArray(EXTRA_FACILITATOR_ICON, currentCall.getAppIcon());
760                     final PhoneAccountHandle handle =
761                             mPhoneAccountManager.getPhoneAccountHandle(
762                                     associationId,
763                                     currentCall.getFacilitator().getIdentifier());
764                     if (currentCall.getDirection() == android.companion.Telecom.Call.INCOMING) {
765                         mTelecomManager.addNewIncomingCall(handle, extras);
766                     } else if (currentCall.getDirection()
767                             == android.companion.Telecom.Call.OUTGOING) {
768                         final Bundle wrappedExtras = new Bundle();
769                         wrappedExtras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
770                                 extras);
771                         wrappedExtras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
772                                 handle);
773                         final String address = currentCall.getCallerId();
774                         if (address != null) {
775                             mTelecomManager.placeCall(Uri.fromParts(PhoneAccount.SCHEME_SIP,
776                                     address, /* fragment= */ null), wrappedExtras);
777                         }
778                     }
779                 }
780             }
781             mCallIds.put(associationId, newCallIds);
782         }
783 
clearCallIdsForAssociationId(int associationId)784         Set<String> clearCallIdsForAssociationId(int associationId) {
785             return mCallIds.remove(associationId);
786         }
787 
addSelfOwnedCallId(String callId)788         void addSelfOwnedCallId(String callId) {
789             mSelfOwnedCalls.add(callId);
790         }
791 
removeSelfOwnedCallId(String callId)792         void removeSelfOwnedCallId(String callId) {
793             mSelfOwnedCalls.remove(callId);
794         }
795 
isExternallyOwned(String callId)796         boolean isExternallyOwned(String callId) {
797             return mExternallyOwnedCalls.contains(callId);
798         }
799 
isSelfOwned(String currentCallId)800         private boolean isSelfOwned(String currentCallId) {
801             for (String selfOwnedCallId : mSelfOwnedCalls) {
802                 if (currentCallId.endsWith(selfOwnedCallId)) {
803                     return true;
804                 }
805             }
806             return false;
807         }
808     }
809 
810     static class PhoneAccountManager {
811         private final Map<PhoneAccountHandleIdentifier, PhoneAccountHandle> mPhoneAccountHandles =
812                 new HashMap<>();
813         private final TelecomManager mTelecomManager;
814         private final ComponentName mConnectionServiceComponentName;
815 
PhoneAccountManager(Context context)816         PhoneAccountManager(Context context) {
817             mTelecomManager = context.getSystemService(TelecomManager.class);
818             mConnectionServiceComponentName = new ComponentName(context,
819                     CallMetadataSyncConnectionService.class);
820         }
821 
onBootCompleted()822         void onBootCompleted() {
823             mTelecomManager.clearPhoneAccounts();
824         }
825 
getPhoneAccountHandle(int associationId, String appIdentifier)826         PhoneAccountHandle getPhoneAccountHandle(int associationId, String appIdentifier) {
827             return mPhoneAccountHandles.get(
828                     new PhoneAccountHandleIdentifier(associationId, appIdentifier));
829         }
830 
updateFacilitators(int associationId, CallMetadataSyncData data)831         void updateFacilitators(int associationId, CallMetadataSyncData data) {
832             final ArrayList<CallMetadataSyncData.CallFacilitator> facilitators = new ArrayList<>();
833             for (CallMetadataSyncData.Call call : data.getCalls()) {
834                 facilitators.add(call.getFacilitator());
835             }
836             facilitators.addAll(data.getFacilitators());
837             updateFacilitators(associationId, facilitators);
838         }
839 
updateFacilitators(int associationId, List<CallMetadataSyncData.CallFacilitator> facilitators)840         private void updateFacilitators(int associationId,
841                 List<CallMetadataSyncData.CallFacilitator> facilitators) {
842             final Iterator<PhoneAccountHandleIdentifier> iterator =
843                     mPhoneAccountHandles.keySet().iterator();
844             while (iterator.hasNext()) {
845                 final PhoneAccountHandleIdentifier handleIdentifier = iterator.next();
846                 final String handleAppIdentifier = handleIdentifier.getAppIdentifier();
847                 final int handleAssociationId = handleIdentifier.getAssociationId();
848                 if (associationId == handleAssociationId && facilitators.stream().noneMatch(
849                         facilitator -> handleAppIdentifier != null && handleAppIdentifier.equals(
850                                 facilitator.getIdentifier()))) {
851                     unregisterPhoneAccount(mPhoneAccountHandles.get(handleIdentifier));
852                     iterator.remove();
853                 }
854             }
855 
856             for (CallMetadataSyncData.CallFacilitator facilitator : facilitators) {
857                 final PhoneAccountHandleIdentifier phoneAccountHandleIdentifier =
858                         new PhoneAccountHandleIdentifier(associationId,
859                                 facilitator.getIdentifier());
860                 if (!mPhoneAccountHandles.containsKey(phoneAccountHandleIdentifier)) {
861                     registerPhoneAccount(phoneAccountHandleIdentifier, facilitator.getName(),
862                             facilitator.isTel());
863                 }
864             }
865         }
866 
867         /**
868          * Registers a {@link android.telecom.PhoneAccount} for a given call-capable app on the
869          * synced device, and records it in the local {@link #mPhoneAccountHandles} map.
870          */
registerPhoneAccount(PhoneAccountHandleIdentifier handleIdentifier, String humanReadableAppName, boolean isTel)871         private void registerPhoneAccount(PhoneAccountHandleIdentifier handleIdentifier,
872                 String humanReadableAppName, boolean isTel) {
873             if (mPhoneAccountHandles.containsKey(handleIdentifier)) {
874                 // Already exists!
875                 return;
876             }
877             final PhoneAccountHandle handle = new PhoneAccountHandle(
878                     mConnectionServiceComponentName,
879                     UUID.randomUUID().toString());
880             mPhoneAccountHandles.put(handleIdentifier, handle);
881             final PhoneAccount phoneAccount = createPhoneAccount(handle, humanReadableAppName,
882                     handleIdentifier.getAppIdentifier(), handleIdentifier.getAssociationId(),
883                     isTel);
884             mTelecomManager.registerPhoneAccount(phoneAccount);
885             mTelecomManager.enablePhoneAccount(mPhoneAccountHandles.get(handleIdentifier), true);
886         }
887 
888         /**
889          * Unregisters a {@link android.telecom.PhoneAccount} for a given call-capable app on the
890          * synced device. Does NOT remove it from the {@link #mPhoneAccountHandles} map.
891          */
unregisterPhoneAccount(PhoneAccountHandle phoneAccountHandle)892         private void unregisterPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
893             mTelecomManager.unregisterPhoneAccount(phoneAccountHandle);
894         }
895 
896         @VisibleForTesting
createPhoneAccount(PhoneAccountHandle handle, String humanReadableAppName, String appIdentifier, int associationId, boolean isTel)897         static PhoneAccount createPhoneAccount(PhoneAccountHandle handle,
898                 String humanReadableAppName,
899                 String appIdentifier,
900                 int associationId,
901                 boolean isTel) {
902             final Bundle extras = new Bundle();
903             extras.putString(EXTRA_CALL_FACILITATOR_ID, appIdentifier);
904             extras.putInt(EXTRA_ASSOCIATION_ID, associationId);
905             return new PhoneAccount.Builder(handle, humanReadableAppName)
906                     .setExtras(extras)
907                     .setSupportedUriSchemes(List.of(isTel ? PhoneAccount.SCHEME_TEL
908                             : PhoneAccount.SCHEME_SIP))
909                     .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
910                             | PhoneAccount.CAPABILITY_CONNECTION_MANAGER).build();
911         }
912     }
913 
914     static final class PhoneAccountHandleIdentifier {
915         private final int mAssociationId;
916         private final String mAppIdentifier;
917 
PhoneAccountHandleIdentifier(int associationId, String appIdentifier)918         PhoneAccountHandleIdentifier(int associationId, String appIdentifier) {
919             mAssociationId = associationId;
920             mAppIdentifier = appIdentifier;
921         }
922 
getAssociationId()923         public int getAssociationId() {
924             return mAssociationId;
925         }
926 
getAppIdentifier()927         public String getAppIdentifier() {
928             return mAppIdentifier;
929         }
930 
931         @Override
hashCode()932         public int hashCode() {
933             return Objects.hash(mAssociationId, mAppIdentifier);
934         }
935 
936         @Override
equals(Object other)937         public boolean equals(Object other) {
938             if (other instanceof PhoneAccountHandleIdentifier) {
939                 return ((PhoneAccountHandleIdentifier) other).getAssociationId() == mAssociationId
940                         && mAppIdentifier != null
941                         && mAppIdentifier.equals(
942                         ((PhoneAccountHandleIdentifier) other).getAppIdentifier());
943             }
944             return false;
945         }
946     }
947 }
948