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 com.android.server.telecom;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.UserIdInt;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.pm.ResolveInfo;
29 import android.os.Bundle;
30 import android.os.IBinder;
31 import android.os.RemoteException;
32 import android.os.UserHandle;
33 import android.telecom.BluetoothCallQualityReport;
34 import android.telecom.CallAudioState;
35 import android.telecom.CallDiagnosticService;
36 import android.telecom.ConnectionService;
37 import android.telecom.CallDiagnostics;
38 import android.telecom.DisconnectCause;
39 import android.telecom.InCallService;
40 import android.telecom.Log;
41 import android.telecom.ParcelableCall;
42 import android.telephony.CallQuality;
43 import android.telephony.ims.ImsReasonInfo;
44 import android.text.TextUtils;
45 
46 import com.android.internal.telecom.ICallDiagnosticService;
47 import com.android.internal.util.IndentingPrintWriter;
48 
49 import java.util.List;
50 
51 /**
52  * Responsible for maintaining binding to the {@link CallDiagnosticService} defined by the
53  * {@code call_diagnostic_service_package_name} key in the
54  * {@code packages/services/Telecomm/res/values/config.xml} file.
55  */
56 public class CallDiagnosticServiceController extends CallsManagerListenerBase {
57     /**
58      * Context dependencies for the {@link CallDiagnosticServiceController}.
59      */
60     public interface ContextProxy {
queryIntentServicesAsUser(@onNull Intent intent, int resolveInfoFlags, @UserIdInt int userId)61         List<ResolveInfo> queryIntentServicesAsUser(@NonNull Intent intent,
62                 int resolveInfoFlags, @UserIdInt int userId);
bindServiceAsUser(@onNull @equiresPermission Intent service, @NonNull ServiceConnection conn, int flags, @NonNull UserHandle user)63         boolean bindServiceAsUser(@NonNull @RequiresPermission Intent service,
64                 @NonNull ServiceConnection conn, int flags, @NonNull UserHandle user);
unbindService(@onNull ServiceConnection conn)65         void unbindService(@NonNull ServiceConnection conn);
getCurrentUserHandle()66         UserHandle getCurrentUserHandle();
67     }
68 
69     /**
70      * Listener for {@link Call} events; used to propagate these changes to the
71      * {@link CallDiagnosticService}.
72      */
73     private final Call.Listener mCallListener = new Call.ListenerBase() {
74         @Override
75         public void onConnectionCapabilitiesChanged(Call call) {
76             updateCall(call);
77         }
78 
79         @Override
80         public void onConnectionPropertiesChanged(Call call, boolean didRttChange) {
81             updateCall(call);
82         }
83 
84         /**
85          * Listens for changes to extras reported by a Telecom {@link Call}.
86          *
87          * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
88          * so we will only trigger an update of the call information if the source of the
89          * extras change was a {@link ConnectionService}.
90          *
91          * @param call   The call.
92          * @param source The source of the extras change
93          *               ({@link Call#SOURCE_CONNECTION_SERVICE} or
94          *               {@link Call#SOURCE_INCALL_SERVICE}).
95          * @param extras The extras.
96          */
97         @Override
98         public void onExtrasChanged(Call call, int source, Bundle extras,
99                 String requestingPackageName) {
100             // Do not inform of changes which originated from an InCallService to a CDS.
101             if (source == Call.SOURCE_INCALL_SERVICE) {
102                 return;
103             }
104             updateCall(call);
105         }
106 
107         /**
108          * Listens for changes to extras reported by a Telecom {@link Call}.
109          *
110          * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService}
111          * so we will only trigger an update of the call information if the source of the extras
112          * change was a {@link ConnectionService}.
113          *  @param call The call.
114          * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or
115          *               {@link Call#SOURCE_INCALL_SERVICE}).
116          * @param keys The extra key removed
117          */
118         @Override
119         public void onExtrasRemoved(Call call, int source, List<String> keys) {
120             // Do not inform InCallServices of changes which originated there.
121             if (source == Call.SOURCE_INCALL_SERVICE) {
122                 return;
123             }
124             updateCall(call);
125         }
126 
127         /**
128          * Handles changes to the video state of a call.
129          * @param call
130          * @param previousVideoState
131          * @param newVideoState
132          */
133         @Override
134         public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
135             updateCall(call);
136         }
137 
138         /**
139          * Relays a bluetooth call quality report received from the Bluetooth stack to the
140          * CallDiagnosticService.
141          * @param call The call.
142          * @param report The received report.
143          */
144         @Override
145         public void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report) {
146             handleBluetoothCallQualityReport(call, report);
147         }
148 
149         /**
150          * Relays a device to device message received from Telephony to the CallDiagnosticService.
151          * @param call
152          * @param messageType
153          * @param messageValue
154          */
155         @Override
156         public void onReceivedDeviceToDeviceMessage(Call call, int messageType, int messageValue) {
157             handleReceivedDeviceToDeviceMessage(call, messageType, messageValue);
158         }
159 
160         /**
161          * Handles an incoming {@link CallQuality} report from a {@link android.telecom.Connection}.
162          * @param call The call.
163          * @param callQualityReport The call quality report.
164          */
165         @Override
166         public void onReceivedCallQualityReport(Call call, CallQuality callQualityReport) {
167             handleCallQualityReport(call, callQualityReport);
168         }
169     };
170 
171     /**
172      * {@link ServiceConnection} handling changes to binding of the {@link CallDiagnosticService}.
173      */
174     private class CallDiagnosticServiceConnection implements ServiceConnection {
175         @Override
onServiceConnected(ComponentName name, IBinder service)176         public void onServiceConnected(ComponentName name, IBinder service) {
177             Log.startSession("CDSC.oSC", Log.getPackageAbbreviation(name));
178             try {
179                 synchronized (mLock) {
180                     mCallDiagnosticService = ICallDiagnosticService.Stub.asInterface(service);
181 
182                     handleConnectionComplete(mCallDiagnosticService);
183                 }
184                 Log.i(CallDiagnosticServiceController.this, "onServiceConnected: cmp=%s", name);
185             } finally {
186                 Log.endSession();
187             }
188         }
189 
190         @Override
onServiceDisconnected(ComponentName name)191         public void onServiceDisconnected(ComponentName name) {
192             Log.startSession("CDSC.oSD", Log.getPackageAbbreviation(name));
193             try {
194                 synchronized (mLock) {
195                     mCallDiagnosticService = null;
196                     mConnection = null;
197                 }
198                 Log.i(CallDiagnosticServiceController.this, "onServiceDisconnected: cmp=%s", name);
199             } finally {
200                 Log.endSession();
201             }
202         }
203 
204         @Override
onBindingDied(ComponentName name)205         public void onBindingDied(ComponentName name) {
206             Log.startSession("CDSC.oBD", Log.getPackageAbbreviation(name));
207             try {
208                 synchronized (mLock) {
209                     mCallDiagnosticService = null;
210                     mConnection = null;
211                 }
212                 Log.w(CallDiagnosticServiceController.this, "onBindingDied: cmp=%s", name);
213             } finally {
214                 Log.endSession();
215             }
216         }
217 
218         @Override
onNullBinding(ComponentName name)219         public void onNullBinding(ComponentName name) {
220             Log.startSession("CDSC.oNB", Log.getPackageAbbreviation(name));
221             try {
222                 synchronized (mLock) {
223                     maybeUnbindCallScreeningService();
224                 }
225             } finally {
226                 Log.endSession();
227             }
228         }
229     }
230 
231     private final String mPackageName;
232     private final ContextProxy mContextProxy;
233     private InCallTonePlayer.Factory mPlayerFactory;
234     private String mTestPackageName;
235     private CallDiagnosticServiceConnection mConnection;
236     private CallDiagnosticServiceAdapter mAdapter;
237     private final TelecomSystem.SyncRoot mLock;
238     private ICallDiagnosticService mCallDiagnosticService;
239     private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
240 
CallDiagnosticServiceController(@onNull ContextProxy contextProxy, @Nullable String packageName, @NonNull TelecomSystem.SyncRoot lock)241     public CallDiagnosticServiceController(@NonNull ContextProxy contextProxy,
242             @Nullable String packageName, @NonNull TelecomSystem.SyncRoot lock) {
243         mContextProxy = contextProxy;
244         mPackageName = packageName;
245         mLock = lock;
246     }
247 
248     /**
249      * Sets the current {@link InCallTonePlayer.Factory} for this instance.
250      * @param factory the factory.
251      */
setInCallTonePlayerFactory(InCallTonePlayer.Factory factory)252     public void setInCallTonePlayerFactory(InCallTonePlayer.Factory factory) {
253         mPlayerFactory = factory;
254     }
255 
256     /**
257      * Handles Telecom adding new calls.  Will bind to the call diagnostic service if needed and
258      * send the calls, or send to an already bound service.
259      * @param call The call to add.
260      */
261     @Override
onCallAdded(@onNull Call call)262     public void onCallAdded(@NonNull Call call) {
263         if (!call.isSimCall() || call.isExternalCall()) {
264             Log.i(this, "onCallAdded: skipping call %s as non-sim or external.", call.getId());
265             return;
266         }
267         if (mCallIdMapper.getCallId(call) == null) {
268             mCallIdMapper.addCall(call);
269             call.addListener(mCallListener);
270         }
271         if (isConnected()) {
272             sendCallToBoundService(call, mCallDiagnosticService);
273         } else {
274             maybeBindCallDiagnosticService();
275         }
276     }
277 
278     /**
279      * Handles a newly disconnected call signalled from {@link CallsManager}.
280      * @param call The call
281      * @param disconnectCause The disconnect cause
282      * @return {@code true} if the {@link CallDiagnosticService} was sent the call, {@code false}
283      * if the call was not applicable to the CDS or if there was an issue sending it.
284      */
onCallDisconnected(@onNull Call call, @NonNull DisconnectCause disconnectCause)285     public boolean onCallDisconnected(@NonNull Call call,
286             @NonNull DisconnectCause disconnectCause) {
287         if (!call.isSimCall() || call.isExternalCall()) {
288             Log.i(this, "onCallDisconnected: skipping call %s as non-sim or external.",
289                     call.getId());
290             return false;
291         }
292         String callId = mCallIdMapper.getCallId(call);
293         try {
294             if (isConnected()) {
295                 mCallDiagnosticService.notifyCallDisconnected(callId, disconnectCause);
296                 return true;
297             }
298         } catch (RemoteException e) {
299             Log.w(this, "onCallDisconnected: callId=%s, exception=%s", call.getId(), e);
300         }
301         return false;
302     }
303 
304     /**
305      * Handles Telecom removal of calls; will remove the call from the bound service and if the
306      * number of tracked calls falls to zero, unbind from the service.
307      * @param call The call to remove from the bound CDS.
308      */
309     @Override
onCallRemoved(@onNull Call call)310     public void onCallRemoved(@NonNull Call call) {
311         if (!call.isSimCall() || call.isExternalCall()) {
312             Log.i(this, "onCallRemoved: skipping call %s as non-sim or external.", call.getId());
313             return;
314         }
315         mCallIdMapper.removeCall(call);
316         call.removeListener(mCallListener);
317         removeCallFromBoundService(call, mCallDiagnosticService);
318 
319         if (mCallIdMapper.getCalls().size() == 0) {
320             maybeUnbindCallScreeningService();
321         }
322     }
323 
324     @Override
onCallStateChanged(Call call, int oldState, int newState)325     public void onCallStateChanged(Call call, int oldState, int newState) {
326         updateCall(call);
327     }
328 
329     @Override
onCallAudioStateChanged(CallAudioState oldCallAudioState, CallAudioState newCallAudioState)330     public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
331             CallAudioState newCallAudioState) {
332         if (mCallDiagnosticService != null) {
333             try {
334                 mCallDiagnosticService.updateCallAudioState(newCallAudioState);
335             } catch (RemoteException e) {
336                 Log.w(this, "onCallAudioStateChanged: failed %s", e);
337             }
338         }
339     }
340 
341     /**
342      * Sets the test call diagnostic service; used by the telecom command line command to override
343      * the {@link CallDiagnosticService} to bind to for CTS test purposes.
344      * @param packageName The package name to set to.
345      */
setTestCallDiagnosticService(@ullable String packageName)346     public void setTestCallDiagnosticService(@Nullable String packageName) {
347         if (TextUtils.isEmpty(packageName)) {
348             mTestPackageName = null;
349         } else {
350             mTestPackageName = packageName;
351         }
352 
353         Log.i(this, "setTestCallDiagnosticService: packageName=%s", packageName);
354     }
355 
356     /**
357      * Determines the active call diagnostic service, taking into account the test override.
358      * @return The package name of the active call diagnostic service.
359      */
getActiveCallDiagnosticService()360     private @Nullable String getActiveCallDiagnosticService() {
361         if (mTestPackageName != null) {
362             return mTestPackageName;
363         }
364 
365         return mPackageName;
366     }
367 
368     /**
369      * If we are not already bound to the {@link CallDiagnosticService}, attempts to initiate a
370      * binding tho that service.
371      * @return {@code true} if we bound, {@code false} otherwise.
372      */
maybeBindCallDiagnosticService()373     private boolean maybeBindCallDiagnosticService() {
374         if (mConnection != null) {
375             return false;
376         }
377 
378         mConnection = new CallDiagnosticServiceConnection();
379         boolean bound = bindCallDiagnosticService(mContextProxy.getCurrentUserHandle(),
380                 getActiveCallDiagnosticService(), mConnection);
381         if (!bound) {
382             mConnection = null;
383         }
384         return bound;
385     }
386 
387     /**
388      * Performs binding to the {@link CallDiagnosticService}.
389      * @param userHandle user name to bind via.
390      * @param packageName package name of the CDS.
391      * @param serviceConnection The service connection to be notified of bind events.
392      * @return
393      */
bindCallDiagnosticService(UserHandle userHandle, String packageName, CallDiagnosticServiceConnection serviceConnection)394     private boolean bindCallDiagnosticService(UserHandle userHandle,
395             String packageName, CallDiagnosticServiceConnection serviceConnection) {
396 
397         if (TextUtils.isEmpty(packageName)) {
398             Log.i(this, "bindCallDiagnosticService: no package; skip binding.");
399             return false;
400         }
401 
402         Intent intent = new Intent(CallDiagnosticService.SERVICE_INTERFACE)
403                 .setPackage(packageName);
404         Log.i(this, "bindCallDiagnosticService: user %d.", userHandle.getIdentifier());
405         List<ResolveInfo> entries = mContextProxy.queryIntentServicesAsUser(intent, 0,
406                 userHandle.getIdentifier());
407         if (entries.isEmpty()) {
408             Log.i(this, "bindCallDiagnosticService: %s has no service.", packageName);
409             return false;
410         }
411 
412         ResolveInfo entry = entries.get(0);
413         if (entry.serviceInfo == null) {
414             Log.i(this, "bindCallDiagnosticService: %s has no service info.", packageName);
415             return false;
416         }
417 
418         if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals(
419                 Manifest.permission.BIND_CALL_DIAGNOSTIC_SERVICE)) {
420             Log.i(this, "bindCallDiagnosticService: %s doesn't require "
421                     + "BIND_CALL_DIAGNOSTIC_SERVICE.", packageName);
422             return false;
423         }
424 
425         ComponentName componentName =
426                 new ComponentName(entry.serviceInfo.packageName, entry.serviceInfo.name);
427         intent.setComponent(componentName);
428         if (mContextProxy.bindServiceAsUser(
429                 intent,
430                 serviceConnection,
431                 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
432                 UserHandle.CURRENT)) {
433             Log.d(this, "bindCallDiagnosticService, found service, waiting for it to connect");
434             return true;
435         }
436         return false;
437     }
438 
439     /**
440      * If we are bound to a {@link CallDiagnosticService}, unbind from it.
441      */
maybeUnbindCallScreeningService()442     public void maybeUnbindCallScreeningService() {
443         if (mConnection != null) {
444             Log.i(this, "maybeUnbindCallScreeningService - unbinding from %s",
445                     getActiveCallDiagnosticService());
446             try {
447                 mContextProxy.unbindService(mConnection);
448                 mCallDiagnosticService = null;
449                 mConnection = null;
450             } catch (IllegalArgumentException e) {
451                 Log.i(this, "maybeUnbindCallScreeningService: Exception when unbind %s : %s",
452                         getActiveCallDiagnosticService(), e.getMessage());
453             }
454         } else {
455             Log.w(this, "maybeUnbindCallScreeningService - already unbound");
456         }
457     }
458 
459     /**
460      * Implements the abstracted Telecom functionality the {@link CallDiagnosticServiceAdapter}
461      * depends on.
462      */
463     private CallDiagnosticServiceAdapter.TelecomAdapter mTelecomAdapter =
464             new CallDiagnosticServiceAdapter.TelecomAdapter() {
465 
466         @Override
467         public void displayDiagnosticMessage(String callId, int messageId, CharSequence message) {
468             handleDisplayDiagnosticMessage(callId, messageId, message);
469         }
470 
471         @Override
472         public void clearDiagnosticMessage(String callId, int messageId) {
473             handleClearDiagnosticMessage(callId, messageId);
474         }
475 
476         @Override
477         public void sendDeviceToDeviceMessage(String callId,
478                         @CallDiagnostics.MessageType int message, int value) {
479             handleSendD2DMessage(callId, message, value);
480         }
481 
482         @Override
483         public void overrideDisconnectMessage(String callId, CharSequence message) {
484             handleOverrideDisconnectMessage(callId, message);
485         }
486     };
487 
488     /**
489      * Sends all calls to the specified {@link CallDiagnosticService}.
490      * @param callDiagnosticService the CDS to send calls to.
491      */
handleConnectionComplete(@onNull ICallDiagnosticService callDiagnosticService)492     private void handleConnectionComplete(@NonNull ICallDiagnosticService callDiagnosticService) {
493         mAdapter = new CallDiagnosticServiceAdapter(mTelecomAdapter,
494                 getActiveCallDiagnosticService(), mLock);
495         try {
496             // Add adapter for communication back from the call diagnostic service to Telecom.
497             callDiagnosticService.setAdapter(mAdapter);
498 
499             // Loop through all the calls we've got ready to send since binding.
500             for (Call call : mCallIdMapper.getCalls()) {
501                 sendCallToBoundService(call, callDiagnosticService);
502             }
503         } catch (RemoteException e) {
504             Log.w(this, "handleConnectionComplete: error=%s", e);
505         }
506     }
507 
508     /**
509      * Handles a request from a {@link CallDiagnosticService} to display a diagnostic message.
510      * @param callId the ID of the call to display the message for.
511      * @param message the message.
512      */
handleDisplayDiagnosticMessage(@onNull String callId, int messageId, @Nullable CharSequence message)513     private void handleDisplayDiagnosticMessage(@NonNull String callId, int messageId,
514             @Nullable CharSequence message) {
515         Call call = mCallIdMapper.getCall(callId);
516         if (call == null) {
517             Log.w(this, "handleDisplayDiagnosticMessage: callId=%s; msg=%d/%s; invalid call",
518                     callId, messageId, message);
519             return;
520         }
521         Log.i(this, "handleDisplayDiagnosticMessage: callId=%s; msg=%d/%s",
522                 callId, messageId, message);
523         if (mPlayerFactory != null) {
524             // Play that tone!
525             mPlayerFactory.createPlayer(call, InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION)
526                     .startTone();
527         }
528         call.displayDiagnosticMessage(messageId, message);
529     }
530 
531     /**
532      * Handles a request from a {@link CallDiagnosticService} to clear a previously displayed
533      * diagnostic message.
534      * @param callId the ID of the call to display the message for.
535      * @param messageId the message ID which was previous posted.
536      */
handleClearDiagnosticMessage(@onNull String callId, int messageId)537     private void handleClearDiagnosticMessage(@NonNull String callId, int messageId) {
538         Call call = mCallIdMapper.getCall(callId);
539         if (call == null) {
540             Log.w(this, "handleClearDiagnosticMessage: callId=%s; msg=%d; invalid call",
541                     callId, messageId);
542             return;
543         }
544         Log.i(this, "handleClearDiagnosticMessage: callId=%s; msg=%d; invalid call",
545                 callId, messageId);
546         call.clearDiagnosticMessage(messageId);
547     }
548 
549     /**
550      * Handles a request from a {@link CallDiagnosticService} to send a device to device message.
551      * @param callId The ID of the call to send the D2D message for.
552      * @param message The message type.
553      * @param value The message value.
554      */
handleSendD2DMessage(@onNull String callId, @CallDiagnostics.MessageType int message, int value)555     private void handleSendD2DMessage(@NonNull String callId,
556             @CallDiagnostics.MessageType int message, int value) {
557         Call call = mCallIdMapper.getCall(callId);
558         if (call == null) {
559             Log.w(this, "handleSendD2DMessage: callId=%s; msg=%d/%d; invalid call", callId,
560                     message, value);
561             return;
562         }
563         Log.i(this, "handleSendD2DMessage: callId=%s; msg=%d/%d", callId, message, value);
564         call.sendDeviceToDeviceMessage(message, value);
565     }
566 
567     /**
568      * Handles a request from a {@link CallDiagnosticService} to override the disconnect message
569      * for a call.  This is the response path from a previous call into the
570      * {@link CallDiagnosticService} via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)}.
571      * @param callId The telecom call ID the disconnect override is pending for.
572      * @param message The new disconnect message, or {@code null} if no override.
573      */
handleOverrideDisconnectMessage(@onNull String callId, @Nullable CharSequence message)574     private void handleOverrideDisconnectMessage(@NonNull String callId,
575             @Nullable CharSequence message) {
576         Call call = mCallIdMapper.getCall(callId);
577         if (call == null) {
578             Log.w(this, "handleOverrideDisconnectMessage: callId=%s; msg=%s; invalid call", callId,
579                     message);
580             return;
581         }
582         Log.i(this, "handleOverrideDisconnectMessage: callId=%s; msg=%s", callId, message);
583         call.handleOverrideDisconnectMessage(message);
584     }
585 
586     /**
587      * Sends a single call to the bound {@link CallDiagnosticService}.
588      * @param call The call to send.
589      * @param callDiagnosticService The CDS to send it to.
590      */
sendCallToBoundService(@onNull Call call, @NonNull ICallDiagnosticService callDiagnosticService)591     private void sendCallToBoundService(@NonNull Call call,
592             @NonNull ICallDiagnosticService callDiagnosticService) {
593         try {
594             if (isConnected()) {
595                 Log.w(this, "sendCallToBoundService: initializing %s", call.getId());
596                 callDiagnosticService.initializeDiagnosticCall(getParceledCall(call));
597             } else {
598                 Log.w(this, "sendCallToBoundService: not bound, skipping %s", call.getId());
599             }
600         } catch (RemoteException e) {
601             Log.w(this, "sendCallToBoundService: callId=%s, exception=%s", call.getId(), e);
602         }
603     }
604 
605     /**
606      * Removes a call from a bound {@link CallDiagnosticService}.
607      * @param call The call to remove.
608      * @param callDiagnosticService The CDS to remove it from.
609      */
removeCallFromBoundService(@onNull Call call, @NonNull ICallDiagnosticService callDiagnosticService)610     private void removeCallFromBoundService(@NonNull Call call,
611             @NonNull ICallDiagnosticService callDiagnosticService) {
612         try {
613             if (isConnected()) {
614                 callDiagnosticService.removeDiagnosticCall(call.getId());
615             }
616         } catch (RemoteException e) {
617             Log.w(this, "removeCallFromBoundService: callId=%s, exception=%s", call.getId(), e);
618         }
619     }
620 
621     /**
622      * @return {@code true} if the call diagnostic service is bound/connected.
623      */
isConnected()624     public boolean isConnected() {
625         return mCallDiagnosticService != null;
626     }
627 
628     /**
629      * Updates the Call diagnostic service with changes to a call.
630      * @param call The updated call.
631      */
updateCall(@onNull Call call)632     private void updateCall(@NonNull Call call) {
633         try {
634             if (isConnected()) {
635                 mCallDiagnosticService.updateCall(getParceledCall(call));
636             }
637         } catch (RemoteException e) {
638             Log.w(this, "updateCall: callId=%s, exception=%s", call.getId(), e);
639         }
640     }
641 
642     /**
643      * Updates the call diagnostic service with a received bluetooth quality report.
644      * @param call The call.
645      * @param report The bluetooth call quality report.
646      */
handleBluetoothCallQualityReport(@onNull Call call, @NonNull BluetoothCallQualityReport report)647     private void handleBluetoothCallQualityReport(@NonNull Call call,
648             @NonNull BluetoothCallQualityReport report) {
649         try {
650             if (isConnected()) {
651                 mCallDiagnosticService.receiveBluetoothCallQualityReport(report);
652             }
653         } catch (RemoteException e) {
654             Log.w(this, "handleBluetoothCallQualityReport: callId=%s, exception=%s", call.getId(),
655                     e);
656         }
657     }
658 
659     /**
660      * Informs a CallDiagnosticService of an incoming device to device message which was received
661      * via the carrier network.
662      * @param call the call the message was received via.
663      * @param messageType The message type.
664      * @param messageValue The message value.
665      */
handleReceivedDeviceToDeviceMessage(@onNull Call call, int messageType, int messageValue)666     private void handleReceivedDeviceToDeviceMessage(@NonNull Call call, int messageType,
667             int messageValue) {
668         try {
669             if (isConnected()) {
670                 mCallDiagnosticService.receiveDeviceToDeviceMessage(call.getId(), messageType,
671                         messageValue);
672             }
673         } catch (RemoteException e) {
674             Log.w(this, "handleReceivedDeviceToDeviceMessage: callId=%s, exception=%s",
675                     call.getId(), e);
676         }
677     }
678 
679     /**
680      * Handles a reported {@link CallQuality} report from a {@link android.telecom.Connection}.
681      * @param call The call the report originated from.
682      * @param callQualityReport The {@link CallQuality} report.
683      */
handleCallQualityReport(@onNull Call call, @NonNull CallQuality callQualityReport)684     private void handleCallQualityReport(@NonNull Call call,
685             @NonNull CallQuality callQualityReport) {
686         try {
687             if (isConnected()) {
688                 mCallDiagnosticService.callQualityChanged(call.getId(), callQualityReport);
689             }
690         } catch (RemoteException e) {
691             Log.w(this, "handleCallQualityReport: callId=%s, exception=%s",
692                     call.getId(), e);
693         }
694     }
695 
696     /**
697      * Get a parcelled representation of a call for transport to the service.
698      * @param call The call.
699      * @return The parcelled call.
700      */
getParceledCall(@onNull Call call)701     private @NonNull ParcelableCall getParceledCall(@NonNull Call call) {
702         return ParcelableCallUtils.toParcelableCall(
703                 call,
704                 false /* includeVideoProvider */,
705                 null /* phoneAcctRegistrar */,
706                 false /* supportsExternalCalls */,
707                 false /* includeRttCall */,
708                 false /* isForSystemDialer */
709         );
710     }
711 
712     /**
713      * Dumps the state of the {@link CallDiagnosticServiceController}.
714      *
715      * @param pw The {@code IndentingPrintWriter} to write the state to.
716      */
dump(IndentingPrintWriter pw)717     public void dump(IndentingPrintWriter pw) {
718         pw.print("activeCallDiagnosticService: ");
719         pw.println(getActiveCallDiagnosticService());
720         pw.print("isConnected: ");
721         pw.println(isConnected());
722     }
723 }
724