/* * 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. * *

Manifest Declaration

* The following is an example of how to declare the service entry in the * {@link CallDiagnosticService} manifest file: *
 * {@code
 * 
 *      
 *          
 *      
 * 
 * }
 * 
*

*

Threading Model

* 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 mCallByTelecomCallId = new ArrayMap<>(); private final Map 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 not 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. *

* 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. *

* The {@link CallDiagnosticService} returns an implementation of {@link CallDiagnostics} to be * used for the lifespan of this call. *

* 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. *

* 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. *

* Audio state is common to all calls. *

* 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. *

* 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); } } }