1 /*
2  * Copyright (C) 2022 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.annotation.Nullable;
20 import android.companion.AssociationInfo;
21 import android.telecom.Call;
22 import android.telecom.InCallService;
23 import android.telecom.TelecomManager;
24 import android.util.Slog;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.server.LocalServices;
28 import com.android.server.companion.CompanionDeviceConfig;
29 import com.android.server.companion.CompanionDeviceManagerServiceInternal;
30 
31 import java.util.Collection;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.Map;
35 import java.util.stream.Collectors;
36 
37 /**
38  * In-call service to sync call metadata across a user's devices. Note that mute and silence are
39  * global states and apply to all current calls.
40  */
41 public class CallMetadataSyncInCallService extends InCallService {
42 
43     private static final String TAG = "CallMetadataIcs";
44 
45     private CompanionDeviceManagerServiceInternal mCdmsi;
46 
47     @VisibleForTesting
48     final Map<Call, CrossDeviceCall> mCurrentCalls = new HashMap<>();
49     @VisibleForTesting int mNumberOfActiveSyncAssociations;
50     final Call.Callback mTelecomCallback = new Call.Callback() {
51         @Override
52         public void onDetailsChanged(Call call, Call.Details details) {
53             if (mNumberOfActiveSyncAssociations > 0) {
54                 final CrossDeviceCall crossDeviceCall = mCurrentCalls.get(call);
55                 if (crossDeviceCall != null) {
56                     crossDeviceCall.updateCallDetails(details);
57                     sync(getUserId());
58                 } else {
59                     Slog.w(TAG, "Could not update details for nonexistent call");
60                 }
61             }
62         }
63     };
64     final CrossDeviceSyncControllerCallback
65             mCrossDeviceSyncControllerCallback = new CrossDeviceSyncControllerCallback() {
66                 @Override
67                 void processContextSyncMessage(int associationId,
68                         CallMetadataSyncData callMetadataSyncData) {
69                     final Iterator<CallMetadataSyncData.CallControlRequest> iterator =
70                             callMetadataSyncData.getCallControlRequests().iterator();
71                     while (iterator.hasNext()) {
72                         final CallMetadataSyncData.CallControlRequest request = iterator.next();
73                         processCallControlAction(request.getId(), request.getControl());
74                         iterator.remove();
75                     }
76                 }
77 
78                 private void processCallControlAction(String crossDeviceCallId,
79                         int callControlAction) {
80                     final CrossDeviceCall crossDeviceCall = getCallForId(crossDeviceCallId,
81                             mCurrentCalls.values());
82                     switch (callControlAction) {
83                         case android.companion.Telecom.ACCEPT:
84                             if (crossDeviceCall != null) {
85                                 crossDeviceCall.doAccept();
86                             } else {
87                                 Slog.w(TAG, "Failed to process accept action; no matching call");
88                             }
89                             break;
90                         case android.companion.Telecom.REJECT:
91                             if (crossDeviceCall != null) {
92                                 crossDeviceCall.doReject();
93                             } else {
94                                 Slog.w(TAG, "Failed to process reject action; no matching call");
95                             }
96                             break;
97                         case android.companion.Telecom.SILENCE:
98                             doSilence();
99                             break;
100                         case android.companion.Telecom.MUTE:
101                             doMute();
102                             break;
103                         case android.companion.Telecom.UNMUTE:
104                             doUnmute();
105                             break;
106                         case android.companion.Telecom.END:
107                             if (crossDeviceCall != null) {
108                                 crossDeviceCall.doEnd();
109                             } else {
110                                 Slog.w(TAG, "Failed to process end action; no matching call");
111                             }
112                             break;
113                         case android.companion.Telecom.PUT_ON_HOLD:
114                             if (crossDeviceCall != null) {
115                                 crossDeviceCall.doPutOnHold();
116                             } else {
117                                 Slog.w(TAG, "Failed to process hold action; no matching call");
118                             }
119                             break;
120                         case android.companion.Telecom.TAKE_OFF_HOLD:
121                             if (crossDeviceCall != null) {
122                                 crossDeviceCall.doTakeOffHold();
123                             } else {
124                                 Slog.w(TAG, "Failed to process unhold action; no matching call");
125                             }
126                             break;
127                         default:
128                     }
129                 }
130 
131                 @Override
132                 void requestCrossDeviceSync(AssociationInfo associationInfo) {
133                     if (associationInfo.getUserId() == getUserId()) {
134                         sync(associationInfo);
135                     }
136                 }
137 
138                 @Override
139                 void updateNumberOfActiveSyncAssociations(int userId, boolean added) {
140                     if (userId == getUserId()) {
141                         final boolean wasActivelySyncing = mNumberOfActiveSyncAssociations > 0;
142                         if (added) {
143                             mNumberOfActiveSyncAssociations++;
144                         } else {
145                             mNumberOfActiveSyncAssociations--;
146                         }
147                         if (!wasActivelySyncing && mNumberOfActiveSyncAssociations > 0) {
148                             initializeCalls();
149                         } else if (wasActivelySyncing && mNumberOfActiveSyncAssociations <= 0) {
150                             mCurrentCalls.clear();
151                         }
152                     }
153                 }
154     };
155 
156     @Override
onCreate()157     public void onCreate() {
158         super.onCreate();
159         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)) {
160             mCdmsi = LocalServices.getService(CompanionDeviceManagerServiceInternal.class);
161             mCdmsi.registerCallMetadataSyncCallback(mCrossDeviceSyncControllerCallback,
162                     CrossDeviceSyncControllerCallback.TYPE_IN_CALL_SERVICE);
163         }
164     }
165 
initializeCalls()166     private void initializeCalls() {
167         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)
168                 && mNumberOfActiveSyncAssociations > 0) {
169             mCurrentCalls.putAll(getCalls().stream().collect(Collectors.toMap(call -> call,
170                     call -> new CrossDeviceCall(this, call, getCallAudioState()))));
171             mCurrentCalls.keySet().forEach(call -> call.registerCallback(mTelecomCallback,
172                     getMainThreadHandler()));
173             sync(getUserId());
174         }
175     }
176 
177     @Nullable
178     @VisibleForTesting
getCallForId(String crossDeviceCallId, Collection<CrossDeviceCall> calls)179     CrossDeviceCall getCallForId(String crossDeviceCallId, Collection<CrossDeviceCall> calls) {
180         if (crossDeviceCallId == null) {
181             return null;
182         }
183         for (CrossDeviceCall crossDeviceCall : calls) {
184             if (crossDeviceCallId.equals(crossDeviceCall.getId())) {
185                 return crossDeviceCall;
186             }
187         }
188         return null;
189     }
190 
191     @Override
onCallAdded(Call call)192     public void onCallAdded(Call call) {
193         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)
194                 && mNumberOfActiveSyncAssociations > 0) {
195             mCurrentCalls.put(call,
196                     new CrossDeviceCall(this, call, getCallAudioState()));
197             call.registerCallback(mTelecomCallback);
198             sync(getUserId());
199         }
200     }
201 
202     @Override
onCallRemoved(Call call)203     public void onCallRemoved(Call call) {
204         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)
205                 && mNumberOfActiveSyncAssociations > 0) {
206             mCurrentCalls.remove(call);
207             call.unregisterCallback(mTelecomCallback);
208             mCdmsi.removeSelfOwnedCallId(call.getDetails().getExtras().getString(
209                     CrossDeviceSyncController.EXTRA_CALL_ID));
210             sync(getUserId());
211         }
212     }
213 
214     @Override
onMuteStateChanged(boolean isMuted)215     public void onMuteStateChanged(boolean isMuted) {
216         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)
217                 && mNumberOfActiveSyncAssociations > 0) {
218             mCdmsi.sendCrossDeviceSyncMessageToAllDevices(getUserId(),
219                     CrossDeviceSyncController.createCallControlMessage(null, isMuted
220                             ? android.companion.Telecom.MUTE : android.companion.Telecom.UNMUTE));
221         }
222     }
223 
224     @Override
onSilenceRinger()225     public void onSilenceRinger() {
226         if (CompanionDeviceConfig.isEnabled(CompanionDeviceConfig.ENABLE_CONTEXT_SYNC_TELECOM)
227                 && mNumberOfActiveSyncAssociations > 0) {
228             mCdmsi.sendCrossDeviceSyncMessageToAllDevices(getUserId(),
229                     CrossDeviceSyncController.createCallControlMessage(null,
230                             android.companion.Telecom.SILENCE));
231         }
232     }
233 
doMute()234     private void doMute() {
235         setMuted(/* shouldMute= */ true);
236     }
237 
doUnmute()238     private void doUnmute() {
239         setMuted(/* shouldMute= */ false);
240     }
241 
doSilence()242     private void doSilence() {
243         final TelecomManager telecomManager = getSystemService(TelecomManager.class);
244         if (telecomManager != null) {
245             telecomManager.silenceRinger();
246         }
247     }
248 
sync(int userId)249     private void sync(int userId) {
250         mCdmsi.crossDeviceSync(userId, mCurrentCalls.values());
251     }
252 
sync(AssociationInfo associationInfo)253     private void sync(AssociationInfo associationInfo) {
254         mCdmsi.crossDeviceSync(associationInfo, mCurrentCalls.values());
255     }
256 }