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