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