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 }