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