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 android.media.AudioManager; 20 import android.net.Uri; 21 import android.os.Bundle; 22 import android.telecom.Call; 23 import android.telecom.Connection; 24 import android.telecom.ConnectionRequest; 25 import android.telecom.ConnectionService; 26 import android.telecom.DisconnectCause; 27 import android.telecom.PhoneAccount; 28 import android.telecom.PhoneAccountHandle; 29 import android.telecom.TelecomManager; 30 import android.util.Slog; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.server.LocalServices; 34 import com.android.server.companion.CompanionDeviceManagerServiceInternal; 35 36 import java.util.HashMap; 37 import java.util.Map; 38 import java.util.Objects; 39 import java.util.Set; 40 41 /** Service for Telecom to bind to when call metadata is synced between devices. */ 42 public class CallMetadataSyncConnectionService extends ConnectionService { 43 44 private static final String TAG = "CallMetadataSyncConnectionService"; 45 46 @VisibleForTesting 47 AudioManager mAudioManager; 48 @VisibleForTesting 49 TelecomManager mTelecomManager; 50 private CompanionDeviceManagerServiceInternal mCdmsi; 51 @VisibleForTesting 52 final Map<CallMetadataSyncConnectionIdentifier, CallMetadataSyncConnection> 53 mActiveConnections = new HashMap<>(); 54 @VisibleForTesting 55 final CrossDeviceSyncControllerCallback 56 mCrossDeviceSyncControllerCallback = new CrossDeviceSyncControllerCallback() { 57 58 @Override 59 void processContextSyncMessage(int associationId, 60 CallMetadataSyncData callMetadataSyncData) { 61 // Add new calls or update existing calls. 62 for (CallMetadataSyncData.Call call : callMetadataSyncData.getCalls()) { 63 final CallMetadataSyncConnection existingConnection = 64 mActiveConnections.get(new CallMetadataSyncConnectionIdentifier( 65 associationId, call.getId())); 66 if (existingConnection != null) { 67 existingConnection.update(call); 68 } else { 69 // Check if this is an in-progress id being finalized. 70 CallMetadataSyncConnectionIdentifier key = null; 71 for (Map.Entry<CallMetadataSyncConnectionIdentifier, 72 CallMetadataSyncConnection> e : mActiveConnections.entrySet()) { 73 if (e.getValue().getAssociationId() == associationId 74 && !e.getValue().isIdFinalized() 75 && call.getId().endsWith(e.getValue().getCallId())) { 76 key = e.getKey(); 77 break; 78 } 79 } 80 if (key != null) { 81 final CallMetadataSyncConnection connection = 82 mActiveConnections.remove(key); 83 connection.update(call); 84 mActiveConnections.put( 85 new CallMetadataSyncConnectionIdentifier(associationId, 86 call.getId()), connection); 87 } 88 } 89 } 90 // Remove obsolete calls. 91 mActiveConnections.values().removeIf(connection -> { 92 if (connection.isIdFinalized() 93 && associationId == connection.getAssociationId() 94 && !callMetadataSyncData.hasCall(connection.getCallId())) { 95 connection.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 96 return true; 97 } 98 return false; 99 }); 100 } 101 102 @Override 103 void cleanUpCallIds(Set<String> callIds) { 104 mActiveConnections.values().removeIf(connection -> { 105 if (callIds.contains(connection.getCallId())) { 106 connection.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 107 return true; 108 } 109 return false; 110 }); 111 } 112 }; 113 114 @Override onCreate()115 public void onCreate() { 116 super.onCreate(); 117 118 mAudioManager = getSystemService(AudioManager.class); 119 mTelecomManager = getSystemService(TelecomManager.class); 120 mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class); 121 mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback, 122 CrossDeviceSyncControllerCallback.TYPE_CONNECTION_SERVICE); 123 } 124 125 @Override onCreateIncomingConnection(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest)126 public Connection onCreateIncomingConnection(PhoneAccountHandle phoneAccountHandle, 127 ConnectionRequest connectionRequest) { 128 final int associationId = connectionRequest.getExtras().getInt( 129 CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); 130 final CallMetadataSyncData.Call call = CallMetadataSyncData.Call.fromBundle( 131 connectionRequest.getExtras().getBundle(CrossDeviceSyncController.EXTRA_CALL)); 132 call.setDirection(android.companion.Telecom.Call.INCOMING); 133 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL); 134 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID); 135 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); 136 final CallMetadataSyncConnection connection = new CallMetadataSyncConnection( 137 mTelecomManager, 138 mAudioManager, 139 associationId, 140 call, 141 new CallMetadataSyncConnectionCallback() { 142 @Override 143 void sendCallAction(int associationId, String callId, int action) { 144 mCdmsi.sendCrossDeviceSyncMessage(associationId, 145 CrossDeviceSyncController.createCallControlMessage(callId, action)); 146 } 147 }); 148 connection.setConnectionProperties(Connection.PROPERTY_IS_EXTERNAL_CALL); 149 connection.setInitializing(); 150 return connection; 151 } 152 153 @Override onCreateIncomingConnectionFailed(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest)154 public void onCreateIncomingConnectionFailed(PhoneAccountHandle phoneAccountHandle, 155 ConnectionRequest connectionRequest) { 156 final String id = 157 phoneAccountHandle != null ? phoneAccountHandle.getId() : "unknown PhoneAccount"; 158 Slog.e(TAG, "onCreateOutgoingConnectionFailed for: " + id); 159 } 160 161 @Override onCreateOutgoingConnection(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest)162 public Connection onCreateOutgoingConnection(PhoneAccountHandle phoneAccountHandle, 163 ConnectionRequest connectionRequest) { 164 final PhoneAccountHandle handle = phoneAccountHandle != null ? phoneAccountHandle 165 : connectionRequest.getAccountHandle(); 166 final PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(handle); 167 168 final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call(); 169 call.setId( 170 connectionRequest.getExtras().getString(CrossDeviceSyncController.EXTRA_CALL_ID)); 171 call.setStatus(android.companion.Telecom.Call.UNKNOWN_STATUS); 172 final CallMetadataSyncData.CallFacilitator callFacilitator = 173 new CallMetadataSyncData.CallFacilitator(phoneAccount != null 174 ? phoneAccount.getLabel().toString() 175 : handle.getComponentName().getShortClassName(), 176 phoneAccount != null ? phoneAccount.getExtras().getString( 177 CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID) 178 : handle.getComponentName().getPackageName(), 179 handle.getComponentName().flattenToString()); 180 call.setFacilitator(callFacilitator); 181 call.setDirection(android.companion.Telecom.Call.OUTGOING); 182 call.setCallerId(connectionRequest.getAddress().getSchemeSpecificPart()); 183 184 final int associationId = phoneAccount.getExtras().getInt( 185 CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); 186 187 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL); 188 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_CALL_FACILITATOR_ID); 189 connectionRequest.getExtras().remove(CrossDeviceSyncController.EXTRA_ASSOCIATION_ID); 190 191 final CallMetadataSyncConnection connection = new CallMetadataSyncConnection( 192 mTelecomManager, 193 mAudioManager, 194 associationId, 195 call, 196 new CallMetadataSyncConnectionCallback() { 197 @Override 198 void sendCallAction(int associationId, String callId, int action) { 199 mCdmsi.sendCrossDeviceSyncMessage(associationId, 200 CrossDeviceSyncController.createCallControlMessage(callId, action)); 201 } 202 }); 203 connection.setCallerDisplayName(call.getCallerId(), TelecomManager.PRESENTATION_ALLOWED); 204 205 mCdmsi.addSelfOwnedCallId(call.getId()); 206 mCdmsi.sendCrossDeviceSyncMessage(associationId, 207 CrossDeviceSyncController.createCallCreateMessage(call.getId(), 208 connectionRequest.getAddress().toString(), 209 call.getFacilitator().getIdentifier())); 210 211 connection.setInitializing(); 212 return connection; 213 } 214 215 @Override onCreateOutgoingConnectionFailed(PhoneAccountHandle phoneAccountHandle, ConnectionRequest connectionRequest)216 public void onCreateOutgoingConnectionFailed(PhoneAccountHandle phoneAccountHandle, 217 ConnectionRequest connectionRequest) { 218 final String id = 219 phoneAccountHandle != null ? phoneAccountHandle.getId() : "unknown PhoneAccount"; 220 Slog.e(TAG, "onCreateOutgoingConnectionFailed for: " + id); 221 } 222 223 @Override onCreateConnectionComplete(Connection connection)224 public void onCreateConnectionComplete(Connection connection) { 225 if (connection instanceof CallMetadataSyncConnection) { 226 final CallMetadataSyncConnection callMetadataSyncConnection = 227 (CallMetadataSyncConnection) connection; 228 callMetadataSyncConnection.initialize(); 229 mActiveConnections.put(new CallMetadataSyncConnectionIdentifier( 230 callMetadataSyncConnection.getAssociationId(), 231 callMetadataSyncConnection.getCallId()), 232 callMetadataSyncConnection); 233 } 234 } 235 236 @VisibleForTesting 237 static final class CallMetadataSyncConnectionIdentifier { 238 private final int mAssociationId; 239 private final String mCallId; 240 CallMetadataSyncConnectionIdentifier(int associationId, String callId)241 CallMetadataSyncConnectionIdentifier(int associationId, String callId) { 242 mAssociationId = associationId; 243 mCallId = callId; 244 } 245 getAssociationId()246 public int getAssociationId() { 247 return mAssociationId; 248 } 249 getCallId()250 public String getCallId() { 251 return mCallId; 252 } 253 254 @Override hashCode()255 public int hashCode() { 256 return Objects.hash(mAssociationId, mCallId); 257 } 258 259 @Override equals(Object other)260 public boolean equals(Object other) { 261 if (other instanceof CallMetadataSyncConnectionIdentifier) { 262 return ((CallMetadataSyncConnectionIdentifier) other).getAssociationId() 263 == mAssociationId 264 && mCallId != null && mCallId.equals( 265 ((CallMetadataSyncConnectionIdentifier) other).getCallId()); 266 } 267 return false; 268 } 269 } 270 271 @VisibleForTesting 272 abstract static class CallMetadataSyncConnectionCallback { 273 sendCallAction(int associationId, String callId, int action)274 abstract void sendCallAction(int associationId, String callId, int action); 275 } 276 277 @VisibleForTesting 278 static class CallMetadataSyncConnection extends Connection { 279 280 private final TelecomManager mTelecomManager; 281 private final AudioManager mAudioManager; 282 private final int mAssociationId; 283 private final CallMetadataSyncData.Call mCall; 284 private final CallMetadataSyncConnectionCallback mCallback; 285 private boolean mIsIdFinalized; 286 CallMetadataSyncConnection(TelecomManager telecomManager, AudioManager audioManager, int associationId, CallMetadataSyncData.Call call, CallMetadataSyncConnectionCallback callback)287 CallMetadataSyncConnection(TelecomManager telecomManager, AudioManager audioManager, 288 int associationId, CallMetadataSyncData.Call call, 289 CallMetadataSyncConnectionCallback callback) { 290 mTelecomManager = telecomManager; 291 mAudioManager = audioManager; 292 mAssociationId = associationId; 293 mCall = call; 294 mCallback = callback; 295 } 296 getCallId()297 public String getCallId() { 298 return mCall.getId(); 299 } 300 getAssociationId()301 public int getAssociationId() { 302 return mAssociationId; 303 } 304 isIdFinalized()305 public boolean isIdFinalized() { 306 return mIsIdFinalized; 307 } 308 initialize()309 private void initialize() { 310 final int status = mCall.getStatus(); 311 if (status == android.companion.Telecom.Call.RINGING_SILENCED) { 312 mTelecomManager.silenceRinger(); 313 } 314 final int state = CrossDeviceCall.convertStatusToState(status); 315 if (state == Call.STATE_RINGING) { 316 setRinging(); 317 } else if (state == Call.STATE_ACTIVE) { 318 setActive(); 319 } else if (state == Call.STATE_HOLDING) { 320 setOnHold(); 321 } else if (state == Call.STATE_DISCONNECTED) { 322 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 323 } else if (state == Call.STATE_DIALING) { 324 setDialing(); 325 } else { 326 setInitialized(); 327 } 328 329 final String callerId = mCall.getCallerId(); 330 if (callerId != null) { 331 setCallerDisplayName(callerId, TelecomManager.PRESENTATION_ALLOWED); 332 setAddress(Uri.fromParts("custom", mCall.getCallerId(), null), 333 TelecomManager.PRESENTATION_ALLOWED); 334 } 335 336 final Bundle extras = new Bundle(); 337 extras.putString(CrossDeviceSyncController.EXTRA_CALL_ID, mCall.getId()); 338 putExtras(extras); 339 340 int capabilities = getConnectionCapabilities(); 341 if (mCall.hasControl(android.companion.Telecom.PUT_ON_HOLD)) { 342 capabilities |= CAPABILITY_HOLD; 343 } else { 344 capabilities &= ~CAPABILITY_HOLD; 345 } 346 if (mCall.hasControl(android.companion.Telecom.MUTE)) { 347 capabilities |= CAPABILITY_MUTE; 348 } else { 349 capabilities &= ~CAPABILITY_MUTE; 350 } 351 mAudioManager.setMicrophoneMute( 352 mCall.hasControl(android.companion.Telecom.UNMUTE)); 353 if (capabilities != getConnectionCapabilities()) { 354 setConnectionCapabilities(capabilities); 355 } 356 } 357 update(CallMetadataSyncData.Call call)358 private void update(CallMetadataSyncData.Call call) { 359 if (!mIsIdFinalized) { 360 mCall.setId(call.getId()); 361 mIsIdFinalized = true; 362 } 363 final int status = call.getStatus(); 364 if (status == android.companion.Telecom.Call.RINGING_SILENCED 365 && mCall.getStatus() != android.companion.Telecom.Call.RINGING_SILENCED) { 366 mTelecomManager.silenceRinger(); 367 } 368 mCall.setStatus(status); 369 final int state = CrossDeviceCall.convertStatusToState(status); 370 if (state != getState()) { 371 if (state == Call.STATE_RINGING) { 372 setRinging(); 373 } else if (state == Call.STATE_ACTIVE) { 374 setActive(); 375 } else if (state == Call.STATE_HOLDING) { 376 setOnHold(); 377 } else if (state == Call.STATE_DISCONNECTED) { 378 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 379 } else if (state == Call.STATE_DIALING) { 380 setDialing(); 381 } else { 382 Slog.e(TAG, "Could not update call to unknown state"); 383 } 384 } 385 386 int capabilities = getConnectionCapabilities(); 387 mCall.setControls(call.getControls()); 388 final boolean hasHoldControl = mCall.hasControl( 389 android.companion.Telecom.PUT_ON_HOLD) 390 || mCall.hasControl(android.companion.Telecom.TAKE_OFF_HOLD); 391 if (hasHoldControl) { 392 capabilities |= CAPABILITY_HOLD; 393 } else { 394 capabilities &= ~CAPABILITY_HOLD; 395 } 396 final boolean hasMuteControl = mCall.hasControl(android.companion.Telecom.MUTE) 397 || mCall.hasControl(android.companion.Telecom.UNMUTE); 398 if (hasMuteControl) { 399 capabilities |= CAPABILITY_MUTE; 400 } else { 401 capabilities &= ~CAPABILITY_MUTE; 402 } 403 mAudioManager.setMicrophoneMute( 404 mCall.hasControl(android.companion.Telecom.UNMUTE)); 405 if (capabilities != getConnectionCapabilities()) { 406 setConnectionCapabilities(capabilities); 407 } 408 } 409 410 @Override onAnswer(int videoState)411 public void onAnswer(int videoState) { 412 sendCallAction(android.companion.Telecom.ACCEPT); 413 } 414 415 @Override onReject()416 public void onReject() { 417 sendCallAction(android.companion.Telecom.REJECT); 418 } 419 420 @Override onReject(int rejectReason)421 public void onReject(int rejectReason) { 422 onReject(); 423 } 424 425 @Override onReject(String replyMessage)426 public void onReject(String replyMessage) { 427 onReject(); 428 } 429 430 @Override onSilence()431 public void onSilence() { 432 sendCallAction(android.companion.Telecom.SILENCE); 433 } 434 435 @Override onHold()436 public void onHold() { 437 sendCallAction(android.companion.Telecom.PUT_ON_HOLD); 438 } 439 440 @Override onUnhold()441 public void onUnhold() { 442 sendCallAction(android.companion.Telecom.TAKE_OFF_HOLD); 443 } 444 445 @Override onMuteStateChanged(boolean isMuted)446 public void onMuteStateChanged(boolean isMuted) { 447 sendCallAction(isMuted ? android.companion.Telecom.MUTE 448 : android.companion.Telecom.UNMUTE); 449 } 450 451 @Override onDisconnect()452 public void onDisconnect() { 453 sendCallAction(android.companion.Telecom.END); 454 } 455 sendCallAction(int action)456 private void sendCallAction(int action) { 457 mCallback.sendCallAction(mAssociationId, mCall.getId(), action); 458 } 459 } 460 } 461