1 /* 2 * Copyright (C) 2021 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 android.telecom; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SdkConstant; 22 import android.annotation.SuppressLint; 23 import android.annotation.SystemApi; 24 import android.app.Service; 25 import android.content.Intent; 26 import android.os.Handler; 27 import android.os.HandlerExecutor; 28 import android.os.IBinder; 29 import android.os.RemoteException; 30 31 import android.telephony.CallQuality; 32 import android.util.ArrayMap; 33 34 import com.android.internal.telecom.ICallDiagnosticService; 35 import com.android.internal.telecom.ICallDiagnosticServiceAdapter; 36 37 import java.util.Map; 38 import java.util.concurrent.Executor; 39 40 /** 41 * The platform supports a single OEM provided {@link CallDiagnosticService}, as defined by the 42 * {@code call_diagnostic_service_package_name} key in the 43 * {@code packages/services/Telecomm/res/values/config.xml} file. An OEM can use this API to help 44 * provide more actionable information about calling issues the user encounters during and after 45 * a call. 46 * 47 * <h1>Manifest Declaration</h1> 48 * The following is an example of how to declare the service entry in the 49 * {@link CallDiagnosticService} manifest file: 50 * <pre> 51 * {@code 52 * <service android:name="your.package.YourCallDiagnosticServiceImplementation" 53 * android:permission="android.permission.BIND_CALL_DIAGNOSTIC_SERVICE"> 54 * <intent-filter> 55 * <action android:name="android.telecom.CallDiagnosticService"/> 56 * </intent-filter> 57 * </service> 58 * } 59 * </pre> 60 * <p> 61 * <h2>Threading Model</h2> 62 * By default, all incoming IPC from Telecom in this service and in the {@link CallDiagnostics} 63 * instances will take place on the main thread. You can override {@link #getExecutor()} in your 64 * implementation to provide your own {@link Executor}. 65 * @hide 66 */ 67 @SystemApi 68 public abstract class CallDiagnosticService extends Service { 69 70 /** 71 * Binder stub implementation which handles incoming requests from Telecom. 72 */ 73 private final class CallDiagnosticServiceBinder extends ICallDiagnosticService.Stub { 74 75 @Override setAdapter(ICallDiagnosticServiceAdapter adapter)76 public void setAdapter(ICallDiagnosticServiceAdapter adapter) throws RemoteException { 77 handleSetAdapter(adapter); 78 } 79 80 @Override initializeDiagnosticCall(ParcelableCall call)81 public void initializeDiagnosticCall(ParcelableCall call) throws RemoteException { 82 handleCallAdded(call); 83 } 84 85 @Override updateCall(ParcelableCall call)86 public void updateCall(ParcelableCall call) throws RemoteException { 87 handleCallUpdated(call); 88 } 89 90 @Override removeDiagnosticCall(String callId)91 public void removeDiagnosticCall(String callId) throws RemoteException { 92 handleCallRemoved(callId); 93 } 94 95 @Override updateCallAudioState(CallAudioState callAudioState)96 public void updateCallAudioState(CallAudioState callAudioState) throws RemoteException { 97 getExecutor().execute(() -> onCallAudioStateChanged(callAudioState)); 98 } 99 100 @Override receiveDeviceToDeviceMessage(String callId, int message, int value)101 public void receiveDeviceToDeviceMessage(String callId, int message, int value) { 102 handleReceivedD2DMessage(callId, message, value); 103 } 104 105 @Override receiveBluetoothCallQualityReport(BluetoothCallQualityReport qualityReport)106 public void receiveBluetoothCallQualityReport(BluetoothCallQualityReport qualityReport) 107 throws RemoteException { 108 handleBluetoothCallQualityReport(qualityReport); 109 } 110 111 @Override notifyCallDisconnected(@onNull String callId, @NonNull DisconnectCause disconnectCause)112 public void notifyCallDisconnected(@NonNull String callId, 113 @NonNull DisconnectCause disconnectCause) throws RemoteException { 114 handleCallDisconnected(callId, disconnectCause); 115 } 116 117 @Override callQualityChanged(String callId, CallQuality callQuality)118 public void callQualityChanged(String callId, CallQuality callQuality) 119 throws RemoteException { 120 handleCallQualityChanged(callId, callQuality); 121 } 122 } 123 124 /** 125 * Listens to events raised by a {@link CallDiagnostics}. 126 */ 127 private CallDiagnostics.Listener mDiagnosticCallListener = 128 new CallDiagnostics.Listener() { 129 130 @Override 131 public void onSendDeviceToDeviceMessage(CallDiagnostics callDiagnostics, 132 @CallDiagnostics.MessageType int message, int value) { 133 handleSendDeviceToDeviceMessage(callDiagnostics, message, value); 134 } 135 136 @Override 137 public void onDisplayDiagnosticMessage(CallDiagnostics callDiagnostics, 138 int messageId, 139 CharSequence message) { 140 handleDisplayDiagnosticMessage(callDiagnostics, messageId, message); 141 } 142 143 @Override 144 public void onClearDiagnosticMessage(CallDiagnostics callDiagnostics, 145 int messageId) { 146 handleClearDiagnosticMessage(callDiagnostics, messageId); 147 } 148 }; 149 150 /** 151 * The {@link Intent} that must be declared as handled by the service. 152 */ 153 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 154 public static final String SERVICE_INTERFACE = "android.telecom.CallDiagnosticService"; 155 156 /** 157 * Map which tracks the Telecom calls received from the Telecom stack. 158 */ 159 private final Map<String, Call.Details> mCallByTelecomCallId = new ArrayMap<>(); 160 private final Map<String, CallDiagnostics> mDiagnosticCallByTelecomCallId = new ArrayMap<>(); 161 private final Object mLock = new Object(); 162 private ICallDiagnosticServiceAdapter mAdapter; 163 164 /** 165 * Handles binding to the {@link CallDiagnosticService}. 166 * 167 * @param intent The Intent that was used to bind to this service, 168 * as given to {@link android.content.Context#bindService 169 * Context.bindService}. Note that any extras that were included with 170 * the Intent at that point will <em>not</em> be seen here. 171 * @return 172 */ 173 @Nullable 174 @Override onBind(@onNull Intent intent)175 public IBinder onBind(@NonNull Intent intent) { 176 Log.i(this, "onBind!"); 177 return new CallDiagnosticServiceBinder(); 178 } 179 180 /** 181 * Returns the {@link Executor} to use for incoming IPS from Telecom into your service 182 * implementation. 183 * <p> 184 * Override this method in your {@link CallDiagnosticService} implementation to provide the 185 * executor you want to use for incoming IPC. 186 * 187 * @return the {@link Executor} to use for incoming IPC from Telecom to 188 * {@link CallDiagnosticService} and {@link CallDiagnostics}. 189 */ 190 @SuppressLint("OnNameExpected") getExecutor()191 @NonNull public Executor getExecutor() { 192 return new HandlerExecutor(Handler.createAsync(getMainLooper())); 193 } 194 195 /** 196 * Telecom calls this method on the {@link CallDiagnosticService} with details about a new call 197 * which was added to Telecom. 198 * <p> 199 * The {@link CallDiagnosticService} returns an implementation of {@link CallDiagnostics} to be 200 * used for the lifespan of this call. 201 * <p> 202 * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see 203 * {@link CallDiagnosticService#getExecutor()} for more information. 204 * 205 * @param call The details of the new call. 206 * @return An instance of {@link CallDiagnostics} which the {@link CallDiagnosticService} 207 * provides to be used for the lifespan of the call. 208 * @throws IllegalArgumentException if a {@code null} {@link CallDiagnostics} is returned. 209 */ onInitializeCallDiagnostics(@onNull android.telecom.Call.Details call)210 public abstract @NonNull CallDiagnostics onInitializeCallDiagnostics(@NonNull 211 android.telecom.Call.Details call); 212 213 /** 214 * Telecom calls this method when a previous created {@link CallDiagnostics} is no longer 215 * needed. This happens when Telecom is no longer tracking the call in question. 216 * <p> 217 * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see 218 * {@link CallDiagnosticService#getExecutor()} for more information. 219 * 220 * @param call The diagnostic call which is no longer tracked by Telecom. 221 */ onRemoveCallDiagnostics(@onNull CallDiagnostics call)222 public abstract void onRemoveCallDiagnostics(@NonNull CallDiagnostics call); 223 224 /** 225 * Telecom calls this method when the audio routing or available audio route information 226 * changes. 227 * <p> 228 * Audio state is common to all calls. 229 * <p> 230 * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see 231 * {@link CallDiagnosticService#getExecutor()} for more information. 232 * 233 * @param audioState The new audio state. 234 */ onCallAudioStateChanged( @onNull CallAudioState audioState)235 public abstract void onCallAudioStateChanged( 236 @NonNull CallAudioState audioState); 237 238 /** 239 * Telecom calls this method when a {@link BluetoothCallQualityReport} is received from the 240 * bluetooth stack. 241 * <p> 242 * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see 243 * {@link CallDiagnosticService#getExecutor()} for more information. 244 * 245 * @param qualityReport the {@link BluetoothCallQualityReport}. 246 */ onBluetoothCallQualityReportReceived( @onNull BluetoothCallQualityReport qualityReport)247 public abstract void onBluetoothCallQualityReportReceived( 248 @NonNull BluetoothCallQualityReport qualityReport); 249 250 /** 251 * Handles a request from Telecom to set the adapater used to communicate back to Telecom. 252 * @param adapter 253 */ handleSetAdapter(@onNull ICallDiagnosticServiceAdapter adapter)254 private void handleSetAdapter(@NonNull ICallDiagnosticServiceAdapter adapter) { 255 mAdapter = adapter; 256 } 257 258 /** 259 * Handles a request from Telecom to add a new call. 260 * @param parcelableCall 261 */ handleCallAdded(@onNull ParcelableCall parcelableCall)262 private void handleCallAdded(@NonNull ParcelableCall parcelableCall) { 263 String telecomCallId = parcelableCall.getId(); 264 Log.i(this, "handleCallAdded: callId=%s - added", telecomCallId); 265 Call.Details newCallDetails = Call.Details.createFromParcelableCall(parcelableCall); 266 synchronized (mLock) { 267 mCallByTelecomCallId.put(telecomCallId, newCallDetails); 268 } 269 270 getExecutor().execute(() -> { 271 CallDiagnostics callDiagnostics = onInitializeCallDiagnostics(newCallDetails); 272 if (callDiagnostics == null) { 273 throw new IllegalArgumentException( 274 "A valid DiagnosticCall instance was not provided."); 275 } 276 synchronized (mLock) { 277 callDiagnostics.setListener(mDiagnosticCallListener); 278 callDiagnostics.setCallId(telecomCallId); 279 mDiagnosticCallByTelecomCallId.put(telecomCallId, callDiagnostics); 280 } 281 }); 282 } 283 284 /** 285 * Handles an update to {@link Call.Details} notified by Telecom. 286 * Caches the call details and notifies the {@link CallDiagnostics} of the change via 287 * {@link CallDiagnostics#onCallDetailsChanged(Call.Details)}. 288 * @param parcelableCall the new parceled call details from Telecom. 289 */ handleCallUpdated(@onNull ParcelableCall parcelableCall)290 private void handleCallUpdated(@NonNull ParcelableCall parcelableCall) { 291 String telecomCallId = parcelableCall.getId(); 292 Log.i(this, "handleCallUpdated: callId=%s - updated", telecomCallId); 293 Call.Details newCallDetails = Call.Details.createFromParcelableCall(parcelableCall); 294 CallDiagnostics callDiagnostics; 295 synchronized (mLock) { 296 callDiagnostics = mDiagnosticCallByTelecomCallId.get(telecomCallId); 297 if (callDiagnostics == null) { 298 // Possible to get a call update after a call is removed. 299 return; 300 } 301 mCallByTelecomCallId.put(telecomCallId, newCallDetails); 302 } 303 getExecutor().execute(() -> callDiagnostics.handleCallUpdated(newCallDetails)); 304 } 305 306 /** 307 * Handles a request from Telecom to remove an existing call. 308 * @param telecomCallId 309 */ handleCallRemoved(@onNull String telecomCallId)310 private void handleCallRemoved(@NonNull String telecomCallId) { 311 Log.i(this, "handleCallRemoved: callId=%s - removed", telecomCallId); 312 313 CallDiagnostics callDiagnostics; 314 synchronized (mLock) { 315 if (mCallByTelecomCallId.containsKey(telecomCallId)) { 316 mCallByTelecomCallId.remove(telecomCallId); 317 } 318 319 if (mDiagnosticCallByTelecomCallId.containsKey(telecomCallId)) { 320 callDiagnostics = mDiagnosticCallByTelecomCallId.remove(telecomCallId); 321 } else { 322 callDiagnostics = null; 323 } 324 } 325 326 // Inform the service of the removed call. 327 if (callDiagnostics != null) { 328 getExecutor().execute(() -> onRemoveCallDiagnostics(callDiagnostics)); 329 } 330 } 331 332 /** 333 * Handles an incoming device to device message received from Telecom. Notifies the 334 * {@link CallDiagnostics} via {@link CallDiagnostics#onReceiveDeviceToDeviceMessage(int, int)}. 335 * @param callId 336 * @param message 337 * @param value 338 */ handleReceivedD2DMessage(@onNull String callId, int message, int value)339 private void handleReceivedD2DMessage(@NonNull String callId, int message, int value) { 340 Log.i(this, "handleReceivedD2DMessage: callId=%s, msg=%d/%d", callId, message, value); 341 CallDiagnostics callDiagnostics; 342 synchronized (mLock) { 343 callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); 344 } 345 if (callDiagnostics != null) { 346 getExecutor().execute( 347 () -> callDiagnostics.onReceiveDeviceToDeviceMessage(message, value)); 348 } 349 } 350 351 /** 352 * Handles a request from the Telecom framework to get a disconnect message from the 353 * {@link CallDiagnosticService}. 354 * @param callId The ID of the call. 355 * @param disconnectCause The telecom disconnect cause. 356 */ handleCallDisconnected(@onNull String callId, @NonNull DisconnectCause disconnectCause)357 private void handleCallDisconnected(@NonNull String callId, 358 @NonNull DisconnectCause disconnectCause) { 359 Log.i(this, "handleCallDisconnected: call=%s; cause=%s", callId, disconnectCause); 360 CallDiagnostics callDiagnostics; 361 synchronized (mLock) { 362 callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); 363 } 364 CharSequence message; 365 if (disconnectCause.getImsReasonInfo() != null) { 366 message = callDiagnostics.onCallDisconnected(disconnectCause.getImsReasonInfo()); 367 } else { 368 message = callDiagnostics.onCallDisconnected( 369 disconnectCause.getTelephonyDisconnectCause(), 370 disconnectCause.getTelephonyPreciseDisconnectCause()); 371 } 372 try { 373 mAdapter.overrideDisconnectMessage(callId, message); 374 } catch (RemoteException e) { 375 Log.w(this, "handleCallDisconnected: call=%s; cause=%s; %s", 376 callId, disconnectCause, e); 377 } 378 } 379 380 /** 381 * Handles an incoming bluetooth call quality report from Telecom. Notifies via 382 * {@link CallDiagnosticService#onBluetoothCallQualityReportReceived( 383 * BluetoothCallQualityReport)}. 384 * @param qualityReport The bluetooth call quality remote. 385 */ handleBluetoothCallQualityReport(@onNull BluetoothCallQualityReport qualityReport)386 private void handleBluetoothCallQualityReport(@NonNull BluetoothCallQualityReport 387 qualityReport) { 388 Log.i(this, "handleBluetoothCallQualityReport; report=%s", qualityReport); 389 getExecutor().execute(() -> onBluetoothCallQualityReportReceived(qualityReport)); 390 } 391 392 /** 393 * Handles a change reported by Telecom to the call quality for a call. 394 * @param callId the call ID the change applies to. 395 * @param callQuality The new call quality. 396 */ handleCallQualityChanged(@onNull String callId, @NonNull CallQuality callQuality)397 private void handleCallQualityChanged(@NonNull String callId, 398 @NonNull CallQuality callQuality) { 399 Log.i(this, "handleCallQualityChanged; call=%s, cq=%s", callId, callQuality); 400 CallDiagnostics callDiagnostics; 401 synchronized(mLock) { 402 callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); 403 } 404 if (callDiagnostics != null) { 405 callDiagnostics.onCallQualityReceived(callQuality); 406 } 407 } 408 409 /** 410 * Handles a request from a {@link CallDiagnostics} to send a device to device message (received 411 * via {@link CallDiagnostics#sendDeviceToDeviceMessage(int, int)}. 412 * @param callDiagnostics 413 * @param message 414 * @param value 415 */ handleSendDeviceToDeviceMessage(@onNull CallDiagnostics callDiagnostics, int message, int value)416 private void handleSendDeviceToDeviceMessage(@NonNull CallDiagnostics callDiagnostics, 417 int message, int value) { 418 String callId = callDiagnostics.getCallId(); 419 try { 420 mAdapter.sendDeviceToDeviceMessage(callId, message, value); 421 Log.i(this, "handleSendDeviceToDeviceMessage: call=%s; msg=%d/%d", callId, message, 422 value); 423 } catch (RemoteException e) { 424 Log.w(this, "handleSendDeviceToDeviceMessage: call=%s; msg=%d/%d failed %s", 425 callId, message, value, e); 426 } 427 } 428 429 /** 430 * Handles a request from a {@link CallDiagnostics} to display an in-call diagnostic message. 431 * Originates from {@link CallDiagnostics#displayDiagnosticMessage(int, CharSequence)}. 432 * @param callDiagnostics 433 * @param messageId 434 * @param message 435 */ handleDisplayDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId, CharSequence message)436 private void handleDisplayDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId, 437 CharSequence message) { 438 String callId = callDiagnostics.getCallId(); 439 try { 440 mAdapter.displayDiagnosticMessage(callId, messageId, message); 441 Log.i(this, "handleDisplayDiagnosticMessage: call=%s; msg=%d/%s", callId, messageId, 442 message); 443 } catch (RemoteException e) { 444 Log.w(this, "handleDisplayDiagnosticMessage: call=%s; msg=%d/%s failed %s", 445 callId, messageId, message, e); 446 } 447 } 448 449 /** 450 * Handles a request from a {@link CallDiagnostics} to clear a previously shown diagnostic 451 * message. 452 * Originates from {@link CallDiagnostics#clearDiagnosticMessage(int)}. 453 * @param callDiagnostics 454 * @param messageId 455 */ handleClearDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId)456 private void handleClearDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId) { 457 String callId = callDiagnostics.getCallId(); 458 try { 459 mAdapter.clearDiagnosticMessage(callId, messageId); 460 Log.i(this, "handleClearDiagnosticMessage: call=%s; msg=%d", callId, messageId); 461 } catch (RemoteException e) { 462 Log.w(this, "handleClearDiagnosticMessage: call=%s; msg=%d failed %s", 463 callId, messageId, e); 464 } 465 } 466 } 467