| /* | 
 |  * Copyright (C) 2021 The Android Open Source Project | 
 |  * | 
 |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
 |  * you may not use this file except in compliance with the License. | 
 |  * You may obtain a copy of the License at | 
 |  * | 
 |  *      http://www.apache.org/licenses/LICENSE-2.0 | 
 |  * | 
 |  * Unless required by applicable law or agreed to in writing, software | 
 |  * distributed under the License is distributed on an "AS IS" BASIS, | 
 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 |  * See the License for the specific language governing permissions and | 
 |  * limitations under the License. | 
 |  */ | 
 |  | 
 | package android.telecom; | 
 |  | 
 | import android.annotation.NonNull; | 
 | import android.annotation.Nullable; | 
 | import android.annotation.SdkConstant; | 
 | import android.annotation.SuppressLint; | 
 | import android.annotation.SystemApi; | 
 | import android.app.Service; | 
 | import android.content.Intent; | 
 | import android.os.Handler; | 
 | import android.os.HandlerExecutor; | 
 | import android.os.IBinder; | 
 | import android.os.RemoteException; | 
 |  | 
 | import android.telephony.CallQuality; | 
 | import android.util.ArrayMap; | 
 |  | 
 | import com.android.internal.telecom.ICallDiagnosticService; | 
 | import com.android.internal.telecom.ICallDiagnosticServiceAdapter; | 
 |  | 
 | import java.util.Map; | 
 | import java.util.concurrent.Executor; | 
 |  | 
 | /** | 
 |  * The platform supports a single OEM provided {@link CallDiagnosticService}, as defined by the | 
 |  * {@code call_diagnostic_service_package_name} key in the | 
 |  * {@code packages/services/Telecomm/res/values/config.xml} file.  An OEM can use this API to help | 
 |  * provide more actionable information about calling issues the user encounters during and after | 
 |  * a call. | 
 |  * | 
 |  * <h1>Manifest Declaration</h1> | 
 |  * The following is an example of how to declare the service entry in the | 
 |  * {@link CallDiagnosticService} manifest file: | 
 |  * <pre> | 
 |  * {@code | 
 |  * <service android:name="your.package.YourCallDiagnosticServiceImplementation" | 
 |  *          android:permission="android.permission.BIND_CALL_DIAGNOSTIC_SERVICE"> | 
 |  *      <intent-filter> | 
 |  *          <action android:name="android.telecom.CallDiagnosticService"/> | 
 |  *      </intent-filter> | 
 |  * </service> | 
 |  * } | 
 |  * </pre> | 
 |  * <p> | 
 |  * <h2>Threading Model</h2> | 
 |  * By default, all incoming IPC from Telecom in this service and in the {@link CallDiagnostics} | 
 |  * instances will take place on the main thread.  You can override {@link #getExecutor()} in your | 
 |  * implementation to provide your own {@link Executor}. | 
 |  * @hide | 
 |  */ | 
 | @SystemApi | 
 | public abstract class CallDiagnosticService extends Service { | 
 |  | 
 |     /** | 
 |      * Binder stub implementation which handles incoming requests from Telecom. | 
 |      */ | 
 |     private final class CallDiagnosticServiceBinder extends ICallDiagnosticService.Stub { | 
 |  | 
 |         @Override | 
 |         public void setAdapter(ICallDiagnosticServiceAdapter adapter) throws RemoteException { | 
 |             handleSetAdapter(adapter); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void initializeDiagnosticCall(ParcelableCall call) throws RemoteException { | 
 |             handleCallAdded(call); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void updateCall(ParcelableCall call) throws RemoteException { | 
 |             handleCallUpdated(call); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void removeDiagnosticCall(String callId) throws RemoteException { | 
 |             handleCallRemoved(callId); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void updateCallAudioState(CallAudioState callAudioState) throws RemoteException { | 
 |             getExecutor().execute(() -> onCallAudioStateChanged(callAudioState)); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void receiveDeviceToDeviceMessage(String callId, int message, int value) { | 
 |             handleReceivedD2DMessage(callId, message, value); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void receiveBluetoothCallQualityReport(BluetoothCallQualityReport qualityReport) | 
 |                 throws RemoteException { | 
 |             handleBluetoothCallQualityReport(qualityReport); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void notifyCallDisconnected(@NonNull String callId, | 
 |                 @NonNull DisconnectCause disconnectCause) throws RemoteException { | 
 |             handleCallDisconnected(callId, disconnectCause); | 
 |         } | 
 |  | 
 |         @Override | 
 |         public void callQualityChanged(String callId, CallQuality callQuality) | 
 |                 throws RemoteException { | 
 |             handleCallQualityChanged(callId, callQuality); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Listens to events raised by a {@link CallDiagnostics}. | 
 |      */ | 
 |     private CallDiagnostics.Listener mDiagnosticCallListener = | 
 |             new CallDiagnostics.Listener() { | 
 |  | 
 |                 @Override | 
 |                 public void onSendDeviceToDeviceMessage(CallDiagnostics callDiagnostics, | 
 |                         @CallDiagnostics.MessageType int message, int value) { | 
 |                     handleSendDeviceToDeviceMessage(callDiagnostics, message, value); | 
 |                 } | 
 |  | 
 |                 @Override | 
 |                 public void onDisplayDiagnosticMessage(CallDiagnostics callDiagnostics, | 
 |                         int messageId, | 
 |                         CharSequence message) { | 
 |                     handleDisplayDiagnosticMessage(callDiagnostics, messageId, message); | 
 |                 } | 
 |  | 
 |                 @Override | 
 |                 public void onClearDiagnosticMessage(CallDiagnostics callDiagnostics, | 
 |                         int messageId) { | 
 |                     handleClearDiagnosticMessage(callDiagnostics, messageId); | 
 |                 } | 
 |             }; | 
 |  | 
 |     /** | 
 |      * The {@link Intent} that must be declared as handled by the service. | 
 |      */ | 
 |     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) | 
 |     public static final String SERVICE_INTERFACE = "android.telecom.CallDiagnosticService"; | 
 |  | 
 |     /** | 
 |      * Map which tracks the Telecom calls received from the Telecom stack. | 
 |      */ | 
 |     private final Map<String, Call.Details> mCallByTelecomCallId = new ArrayMap<>(); | 
 |     private final Map<String, CallDiagnostics> mDiagnosticCallByTelecomCallId = new ArrayMap<>(); | 
 |     private final Object mLock = new Object(); | 
 |     private ICallDiagnosticServiceAdapter mAdapter; | 
 |  | 
 |     /** | 
 |      * Handles binding to the {@link CallDiagnosticService}. | 
 |      * | 
 |      * @param intent The Intent that was used to bind to this service, | 
 |      * as given to {@link android.content.Context#bindService | 
 |      * Context.bindService}.  Note that any extras that were included with | 
 |      * the Intent at that point will <em>not</em> be seen here. | 
 |      * @return | 
 |      */ | 
 |     @Nullable | 
 |     @Override | 
 |     public IBinder onBind(@NonNull Intent intent) { | 
 |         Log.i(this, "onBind!"); | 
 |         return new CallDiagnosticServiceBinder(); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Returns the {@link Executor} to use for incoming IPS from Telecom into your service | 
 |      * implementation. | 
 |      * <p> | 
 |      * Override this method in your {@link CallDiagnosticService} implementation to provide the | 
 |      * executor you want to use for incoming IPC. | 
 |      * | 
 |      * @return the {@link Executor} to use for incoming IPC from Telecom to | 
 |      * {@link CallDiagnosticService} and {@link CallDiagnostics}. | 
 |      */ | 
 |     @SuppressLint("OnNameExpected") | 
 |     @NonNull public Executor getExecutor() { | 
 |         return new HandlerExecutor(Handler.createAsync(getMainLooper())); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Telecom calls this method on the {@link CallDiagnosticService} with details about a new call | 
 |      * which was added to Telecom. | 
 |      * <p> | 
 |      * The {@link CallDiagnosticService} returns an implementation of {@link CallDiagnostics} to be | 
 |      * used for the lifespan of this call. | 
 |      * <p> | 
 |      * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see | 
 |      * {@link CallDiagnosticService#getExecutor()} for more information. | 
 |      * | 
 |      * @param call The details of the new call. | 
 |      * @return An instance of {@link CallDiagnostics} which the {@link CallDiagnosticService} | 
 |      * provides to be used for the lifespan of the call. | 
 |      * @throws IllegalArgumentException if a {@code null} {@link CallDiagnostics} is returned. | 
 |      */ | 
 |     public abstract @NonNull CallDiagnostics onInitializeCallDiagnostics(@NonNull | 
 |             android.telecom.Call.Details call); | 
 |  | 
 |     /** | 
 |      * Telecom calls this method when a previous created {@link CallDiagnostics} is no longer | 
 |      * needed.  This happens when Telecom is no longer tracking the call in question. | 
 |      * <p> | 
 |      * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see | 
 |      * {@link CallDiagnosticService#getExecutor()} for more information. | 
 |      * | 
 |      * @param call The diagnostic call which is no longer tracked by Telecom. | 
 |      */ | 
 |     public abstract void onRemoveCallDiagnostics(@NonNull CallDiagnostics call); | 
 |  | 
 |     /** | 
 |      * Telecom calls this method when the audio routing or available audio route information | 
 |      * changes. | 
 |      * <p> | 
 |      * Audio state is common to all calls. | 
 |      * <p> | 
 |      * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see | 
 |      * {@link CallDiagnosticService#getExecutor()} for more information. | 
 |      * | 
 |      * @param audioState The new audio state. | 
 |      */ | 
 |     public abstract void onCallAudioStateChanged( | 
 |             @NonNull CallAudioState audioState); | 
 |  | 
 |     /** | 
 |      * Telecom calls this method when a {@link BluetoothCallQualityReport} is received from the | 
 |      * bluetooth stack. | 
 |      * <p> | 
 |      * Calls to this method will use the {@link CallDiagnosticService}'s {@link Executor}; see | 
 |      * {@link CallDiagnosticService#getExecutor()} for more information. | 
 |      * | 
 |      * @param qualityReport the {@link BluetoothCallQualityReport}. | 
 |      */ | 
 |     public abstract void onBluetoothCallQualityReportReceived( | 
 |             @NonNull BluetoothCallQualityReport qualityReport); | 
 |  | 
 |     /** | 
 |      * Handles a request from Telecom to set the adapater used to communicate back to Telecom. | 
 |      * @param adapter | 
 |      */ | 
 |     private void handleSetAdapter(@NonNull ICallDiagnosticServiceAdapter adapter) { | 
 |         mAdapter = adapter; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from Telecom to add a new call. | 
 |      * @param parcelableCall | 
 |      */ | 
 |     private void handleCallAdded(@NonNull ParcelableCall parcelableCall) { | 
 |         String telecomCallId = parcelableCall.getId(); | 
 |         Log.i(this, "handleCallAdded: callId=%s - added", telecomCallId); | 
 |         Call.Details newCallDetails = Call.Details.createFromParcelableCall(parcelableCall); | 
 |         synchronized (mLock) { | 
 |             mCallByTelecomCallId.put(telecomCallId, newCallDetails); | 
 |         } | 
 |  | 
 |         getExecutor().execute(() -> { | 
 |             CallDiagnostics callDiagnostics = onInitializeCallDiagnostics(newCallDetails); | 
 |             if (callDiagnostics == null) { | 
 |                 throw new IllegalArgumentException( | 
 |                         "A valid DiagnosticCall instance was not provided."); | 
 |             } | 
 |             synchronized (mLock) { | 
 |                 callDiagnostics.setListener(mDiagnosticCallListener); | 
 |                 callDiagnostics.setCallId(telecomCallId); | 
 |                 mDiagnosticCallByTelecomCallId.put(telecomCallId, callDiagnostics); | 
 |             } | 
 |         }); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles an update to {@link Call.Details} notified by Telecom. | 
 |      * Caches the call details and notifies the {@link CallDiagnostics} of the change via | 
 |      * {@link CallDiagnostics#onCallDetailsChanged(Call.Details)}. | 
 |      * @param parcelableCall the new parceled call details from Telecom. | 
 |      */ | 
 |     private void handleCallUpdated(@NonNull ParcelableCall parcelableCall) { | 
 |         String telecomCallId = parcelableCall.getId(); | 
 |         Log.i(this, "handleCallUpdated: callId=%s - updated", telecomCallId); | 
 |         Call.Details newCallDetails = Call.Details.createFromParcelableCall(parcelableCall); | 
 |         CallDiagnostics callDiagnostics; | 
 |         synchronized (mLock) { | 
 |             callDiagnostics = mDiagnosticCallByTelecomCallId.get(telecomCallId); | 
 |             if (callDiagnostics == null) { | 
 |                 // Possible to get a call update after a call is removed. | 
 |                 return; | 
 |             } | 
 |             mCallByTelecomCallId.put(telecomCallId, newCallDetails); | 
 |         } | 
 |         getExecutor().execute(() -> callDiagnostics.handleCallUpdated(newCallDetails)); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from Telecom to remove an existing call. | 
 |      * @param telecomCallId | 
 |      */ | 
 |     private void handleCallRemoved(@NonNull String telecomCallId) { | 
 |         Log.i(this, "handleCallRemoved: callId=%s - removed", telecomCallId); | 
 |  | 
 |         CallDiagnostics callDiagnostics; | 
 |         synchronized (mLock) { | 
 |             if (mCallByTelecomCallId.containsKey(telecomCallId)) { | 
 |                 mCallByTelecomCallId.remove(telecomCallId); | 
 |             } | 
 |  | 
 |             if (mDiagnosticCallByTelecomCallId.containsKey(telecomCallId)) { | 
 |                 callDiagnostics = mDiagnosticCallByTelecomCallId.remove(telecomCallId); | 
 |             } else { | 
 |                 callDiagnostics = null; | 
 |             } | 
 |         } | 
 |  | 
 |         // Inform the service of the removed call. | 
 |         if (callDiagnostics != null) { | 
 |             getExecutor().execute(() -> onRemoveCallDiagnostics(callDiagnostics)); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles an incoming device to device message received from Telecom.  Notifies the | 
 |      * {@link CallDiagnostics} via {@link CallDiagnostics#onReceiveDeviceToDeviceMessage(int, int)}. | 
 |      * @param callId | 
 |      * @param message | 
 |      * @param value | 
 |      */ | 
 |     private void handleReceivedD2DMessage(@NonNull String callId, int message, int value) { | 
 |         Log.i(this, "handleReceivedD2DMessage: callId=%s, msg=%d/%d", callId, message, value); | 
 |         CallDiagnostics callDiagnostics; | 
 |         synchronized (mLock) { | 
 |             callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); | 
 |         } | 
 |         if (callDiagnostics != null) { | 
 |             getExecutor().execute( | 
 |                     () -> callDiagnostics.onReceiveDeviceToDeviceMessage(message, value)); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from the Telecom framework to get a disconnect message from the | 
 |      * {@link CallDiagnosticService}. | 
 |      * @param callId The ID of the call. | 
 |      * @param disconnectCause The telecom disconnect cause. | 
 |      */ | 
 |     private void handleCallDisconnected(@NonNull String callId, | 
 |             @NonNull DisconnectCause disconnectCause) { | 
 |         Log.i(this, "handleCallDisconnected: call=%s; cause=%s", callId, disconnectCause); | 
 |         CallDiagnostics callDiagnostics; | 
 |         synchronized (mLock) { | 
 |             callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); | 
 |         } | 
 |         CharSequence message; | 
 |         if (disconnectCause.getImsReasonInfo() != null) { | 
 |             message = callDiagnostics.onCallDisconnected(disconnectCause.getImsReasonInfo()); | 
 |         } else { | 
 |             message = callDiagnostics.onCallDisconnected( | 
 |                     disconnectCause.getTelephonyDisconnectCause(), | 
 |                     disconnectCause.getTelephonyPreciseDisconnectCause()); | 
 |         } | 
 |         try { | 
 |             mAdapter.overrideDisconnectMessage(callId, message); | 
 |         } catch (RemoteException e) { | 
 |             Log.w(this, "handleCallDisconnected: call=%s; cause=%s; %s", | 
 |                     callId, disconnectCause, e); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles an incoming bluetooth call quality report from Telecom.  Notifies via | 
 |      * {@link CallDiagnosticService#onBluetoothCallQualityReportReceived( | 
 |      * BluetoothCallQualityReport)}. | 
 |      * @param qualityReport The bluetooth call quality remote. | 
 |      */ | 
 |     private void handleBluetoothCallQualityReport(@NonNull BluetoothCallQualityReport | 
 |             qualityReport) { | 
 |         Log.i(this, "handleBluetoothCallQualityReport; report=%s", qualityReport); | 
 |         getExecutor().execute(() -> onBluetoothCallQualityReportReceived(qualityReport)); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a change reported by Telecom to the call quality for a call. | 
 |      * @param callId the call ID the change applies to. | 
 |      * @param callQuality The new call quality. | 
 |      */ | 
 |     private void handleCallQualityChanged(@NonNull String callId, | 
 |             @NonNull CallQuality callQuality) { | 
 |         Log.i(this, "handleCallQualityChanged; call=%s, cq=%s", callId, callQuality); | 
 |         CallDiagnostics callDiagnostics; | 
 |         synchronized(mLock) { | 
 |             callDiagnostics = mDiagnosticCallByTelecomCallId.get(callId); | 
 |         } | 
 |         if (callDiagnostics != null) { | 
 |             callDiagnostics.onCallQualityReceived(callQuality); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from a {@link CallDiagnostics} to send a device to device message (received | 
 |      * via {@link CallDiagnostics#sendDeviceToDeviceMessage(int, int)}. | 
 |      * @param callDiagnostics | 
 |      * @param message | 
 |      * @param value | 
 |      */ | 
 |     private void handleSendDeviceToDeviceMessage(@NonNull CallDiagnostics callDiagnostics, | 
 |             int message, int value) { | 
 |         String callId = callDiagnostics.getCallId(); | 
 |         try { | 
 |             mAdapter.sendDeviceToDeviceMessage(callId, message, value); | 
 |             Log.i(this, "handleSendDeviceToDeviceMessage: call=%s; msg=%d/%d", callId, message, | 
 |                     value); | 
 |         } catch (RemoteException e) { | 
 |             Log.w(this, "handleSendDeviceToDeviceMessage: call=%s; msg=%d/%d failed %s", | 
 |                     callId, message, value, e); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from a {@link CallDiagnostics} to display an in-call diagnostic message. | 
 |      * Originates from {@link CallDiagnostics#displayDiagnosticMessage(int, CharSequence)}. | 
 |      * @param callDiagnostics | 
 |      * @param messageId | 
 |      * @param message | 
 |      */ | 
 |     private void handleDisplayDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId, | 
 |             CharSequence message) { | 
 |         String callId = callDiagnostics.getCallId(); | 
 |         try { | 
 |             mAdapter.displayDiagnosticMessage(callId, messageId, message); | 
 |             Log.i(this, "handleDisplayDiagnosticMessage: call=%s; msg=%d/%s", callId, messageId, | 
 |                     message); | 
 |         } catch (RemoteException e) { | 
 |             Log.w(this, "handleDisplayDiagnosticMessage: call=%s; msg=%d/%s failed %s", | 
 |                     callId, messageId, message, e); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * Handles a request from a {@link CallDiagnostics} to clear a previously shown diagnostic | 
 |      * message. | 
 |      * Originates from {@link CallDiagnostics#clearDiagnosticMessage(int)}. | 
 |      * @param callDiagnostics | 
 |      * @param messageId | 
 |      */ | 
 |     private void handleClearDiagnosticMessage(CallDiagnostics callDiagnostics, int messageId) { | 
 |         String callId = callDiagnostics.getCallId(); | 
 |         try { | 
 |             mAdapter.clearDiagnosticMessage(callId, messageId); | 
 |             Log.i(this, "handleClearDiagnosticMessage: call=%s; msg=%d", callId, messageId); | 
 |         } catch (RemoteException e) { | 
 |             Log.w(this, "handleClearDiagnosticMessage: call=%s; msg=%d failed %s", | 
 |                     callId, messageId, e); | 
 |         } | 
 |     } | 
 | } |