/* * 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 com.android.server.telecom; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.telecom.BluetoothCallQualityReport; import android.telecom.CallAudioState; import android.telecom.CallDiagnosticService; import android.telecom.ConnectionService; import android.telecom.CallDiagnostics; import android.telecom.DisconnectCause; import android.telecom.InCallService; import android.telecom.Log; import android.telecom.ParcelableCall; import android.telephony.CallQuality; import android.telephony.ims.ImsReasonInfo; import android.text.TextUtils; import com.android.internal.telecom.ICallDiagnosticService; import com.android.internal.util.IndentingPrintWriter; import java.util.List; /** * Responsible for maintaining binding to the {@link CallDiagnosticService} defined by the * {@code call_diagnostic_service_package_name} key in the * {@code packages/services/Telecomm/res/values/config.xml} file. */ public class CallDiagnosticServiceController extends CallsManagerListenerBase { /** * Context dependencies for the {@link CallDiagnosticServiceController}. */ public interface ContextProxy { List queryIntentServicesAsUser(@NonNull Intent intent, @PackageManager.ResolveInfoFlags int flags, @UserIdInt int userId); boolean bindServiceAsUser(@NonNull @RequiresPermission Intent service, @NonNull ServiceConnection conn, int flags, @NonNull UserHandle user); void unbindService(@NonNull ServiceConnection conn); UserHandle getCurrentUserHandle(); } /** * Listener for {@link Call} events; used to propagate these changes to the * {@link CallDiagnosticService}. */ private final Call.Listener mCallListener = new Call.ListenerBase() { @Override public void onConnectionCapabilitiesChanged(Call call) { updateCall(call); } @Override public void onConnectionPropertiesChanged(Call call, boolean didRttChange) { updateCall(call); } /** * Listens for changes to extras reported by a Telecom {@link Call}. * * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService} * so we will only trigger an update of the call information if the source of the extras * change was a {@link ConnectionService}. * * @param call The call. * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or * {@link Call#SOURCE_INCALL_SERVICE}). * @param extras The extras. */ @Override public void onExtrasChanged(Call call, int source, Bundle extras) { // Do not inform InCallServices of changes which originated there. if (source == Call.SOURCE_INCALL_SERVICE) { return; } updateCall(call); } /** * Listens for changes to extras reported by a Telecom {@link Call}. * * Extras changes can originate from a {@link ConnectionService} or an {@link InCallService} * so we will only trigger an update of the call information if the source of the extras * change was a {@link ConnectionService}. * @param call The call. * @param source The source of the extras change ({@link Call#SOURCE_CONNECTION_SERVICE} or * {@link Call#SOURCE_INCALL_SERVICE}). * @param keys The extra key removed */ @Override public void onExtrasRemoved(Call call, int source, List keys) { // Do not inform InCallServices of changes which originated there. if (source == Call.SOURCE_INCALL_SERVICE) { return; } updateCall(call); } /** * Handles changes to the video state of a call. * @param call * @param previousVideoState * @param newVideoState */ @Override public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) { updateCall(call); } /** * Relays a bluetooth call quality report received from the Bluetooth stack to the * CallDiagnosticService. * @param call The call. * @param report The received report. */ @Override public void onBluetoothCallQualityReport(Call call, BluetoothCallQualityReport report) { handleBluetoothCallQualityReport(call, report); } /** * Relays a device to device message received from Telephony to the CallDiagnosticService. * @param call * @param messageType * @param messageValue */ @Override public void onReceivedDeviceToDeviceMessage(Call call, int messageType, int messageValue) { handleReceivedDeviceToDeviceMessage(call, messageType, messageValue); } /** * Handles an incoming {@link CallQuality} report from a {@link android.telecom.Connection}. * @param call The call. * @param callQualityReport The call quality report. */ @Override public void onReceivedCallQualityReport(Call call, CallQuality callQualityReport) { handleCallQualityReport(call, callQualityReport); } }; /** * {@link ServiceConnection} handling changes to binding of the {@link CallDiagnosticService}. */ private class CallDiagnosticServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.startSession("CDSC.oSC", Log.getPackageAbbreviation(name)); try { synchronized (mLock) { mCallDiagnosticService = ICallDiagnosticService.Stub.asInterface(service); handleConnectionComplete(mCallDiagnosticService); } Log.i(CallDiagnosticServiceController.this, "onServiceConnected: cmp=%s", name); } finally { Log.endSession(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.startSession("CDSC.oSD", Log.getPackageAbbreviation(name)); try { synchronized (mLock) { mCallDiagnosticService = null; mConnection = null; } Log.i(CallDiagnosticServiceController.this, "onServiceDisconnected: cmp=%s", name); } finally { Log.endSession(); } } @Override public void onBindingDied(ComponentName name) { Log.startSession("CDSC.oBD", Log.getPackageAbbreviation(name)); try { synchronized (mLock) { mCallDiagnosticService = null; mConnection = null; } Log.w(CallDiagnosticServiceController.this, "onBindingDied: cmp=%s", name); } finally { Log.endSession(); } } @Override public void onNullBinding(ComponentName name) { Log.startSession("CDSC.oNB", Log.getPackageAbbreviation(name)); try { synchronized (mLock) { maybeUnbindCallScreeningService(); } } finally { Log.endSession(); } } } private final String mPackageName; private final ContextProxy mContextProxy; private InCallTonePlayer.Factory mPlayerFactory; private String mTestPackageName; private CallDiagnosticServiceConnection mConnection; private CallDiagnosticServiceAdapter mAdapter; private final TelecomSystem.SyncRoot mLock; private ICallDiagnosticService mCallDiagnosticService; private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId); public CallDiagnosticServiceController(@NonNull ContextProxy contextProxy, @Nullable String packageName, @NonNull TelecomSystem.SyncRoot lock) { mContextProxy = contextProxy; mPackageName = packageName; mLock = lock; } /** * Sets the current {@link InCallTonePlayer.Factory} for this instance. * @param factory the factory. */ public void setInCallTonePlayerFactory(InCallTonePlayer.Factory factory) { mPlayerFactory = factory; } /** * Handles Telecom adding new calls. Will bind to the call diagnostic service if needed and * send the calls, or send to an already bound service. * @param call The call to add. */ @Override public void onCallAdded(@NonNull Call call) { if (!call.isSimCall() || call.isExternalCall()) { Log.i(this, "onCallAdded: skipping call %s as non-sim or external.", call.getId()); return; } if (mCallIdMapper.getCallId(call) == null) { mCallIdMapper.addCall(call); call.addListener(mCallListener); } if (isConnected()) { sendCallToBoundService(call, mCallDiagnosticService); } else { maybeBindCallDiagnosticService(); } } /** * Handles a newly disconnected call signalled from {@link CallsManager}. * @param call The call * @param disconnectCause The disconnect cause * @return {@code true} if the {@link CallDiagnosticService} was sent the call, {@code false} * if the call was not applicable to the CDS or if there was an issue sending it. */ public boolean onCallDisconnected(@NonNull Call call, @NonNull DisconnectCause disconnectCause) { if (!call.isSimCall() || call.isExternalCall()) { Log.i(this, "onCallDisconnected: skipping call %s as non-sim or external.", call.getId()); return false; } String callId = mCallIdMapper.getCallId(call); try { if (isConnected()) { mCallDiagnosticService.notifyCallDisconnected(callId, disconnectCause); return true; } } catch (RemoteException e) { Log.w(this, "onCallDisconnected: callId=%s, exception=%s", call.getId(), e); } return false; } /** * Handles Telecom removal of calls; will remove the call from the bound service and if the * number of tracked calls falls to zero, unbind from the service. * @param call The call to remove from the bound CDS. */ @Override public void onCallRemoved(@NonNull Call call) { if (!call.isSimCall() || call.isExternalCall()) { Log.i(this, "onCallRemoved: skipping call %s as non-sim or external.", call.getId()); return; } mCallIdMapper.removeCall(call); call.removeListener(mCallListener); removeCallFromBoundService(call, mCallDiagnosticService); if (mCallIdMapper.getCalls().size() == 0) { maybeUnbindCallScreeningService(); } } @Override public void onCallStateChanged(Call call, int oldState, int newState) { updateCall(call); } @Override public void onCallAudioStateChanged(CallAudioState oldCallAudioState, CallAudioState newCallAudioState) { if (mCallDiagnosticService != null) { try { mCallDiagnosticService.updateCallAudioState(newCallAudioState); } catch (RemoteException e) { Log.w(this, "onCallAudioStateChanged: failed %s", e); } } } /** * Sets the test call diagnostic service; used by the telecom command line command to override * the {@link CallDiagnosticService} to bind to for CTS test purposes. * @param packageName The package name to set to. */ public void setTestCallDiagnosticService(@Nullable String packageName) { if (TextUtils.isEmpty(packageName)) { mTestPackageName = null; } else { mTestPackageName = packageName; } Log.i(this, "setTestCallDiagnosticService: packageName=%s", packageName); } /** * Determines the active call diagnostic service, taking into account the test override. * @return The package name of the active call diagnostic service. */ private @Nullable String getActiveCallDiagnosticService() { if (mTestPackageName != null) { return mTestPackageName; } return mPackageName; } /** * If we are not already bound to the {@link CallDiagnosticService}, attempts to initiate a * binding tho that service. * @return {@code true} if we bound, {@code false} otherwise. */ private boolean maybeBindCallDiagnosticService() { if (mConnection != null) { return false; } mConnection = new CallDiagnosticServiceConnection(); boolean bound = bindCallDiagnosticService(mContextProxy.getCurrentUserHandle(), getActiveCallDiagnosticService(), mConnection); if (!bound) { mConnection = null; } return bound; } /** * Performs binding to the {@link CallDiagnosticService}. * @param userHandle user name to bind via. * @param packageName package name of the CDS. * @param serviceConnection The service connection to be notified of bind events. * @return */ private boolean bindCallDiagnosticService(UserHandle userHandle, String packageName, CallDiagnosticServiceConnection serviceConnection) { if (TextUtils.isEmpty(packageName)) { Log.i(this, "bindCallDiagnosticService: no package; skip binding."); return false; } Intent intent = new Intent(CallDiagnosticService.SERVICE_INTERFACE) .setPackage(packageName); Log.i(this, "bindCallDiagnosticService: user %d.", userHandle.getIdentifier()); List entries = mContextProxy.queryIntentServicesAsUser(intent, 0, userHandle.getIdentifier()); if (entries.isEmpty()) { Log.i(this, "bindCallDiagnosticService: %s has no service.", packageName); return false; } ResolveInfo entry = entries.get(0); if (entry.serviceInfo == null) { Log.i(this, "bindCallDiagnosticService: %s has no service info.", packageName); return false; } if (entry.serviceInfo.permission == null || !entry.serviceInfo.permission.equals( Manifest.permission.BIND_CALL_DIAGNOSTIC_SERVICE)) { Log.i(this, "bindCallDiagnosticService: %s doesn't require " + "BIND_CALL_DIAGNOSTIC_SERVICE.", packageName); return false; } ComponentName componentName = new ComponentName(entry.serviceInfo.packageName, entry.serviceInfo.name); intent.setComponent(componentName); if (mContextProxy.bindServiceAsUser( intent, serviceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, UserHandle.CURRENT)) { Log.d(this, "bindCallDiagnosticService, found service, waiting for it to connect"); return true; } return false; } /** * If we are bound to a {@link CallDiagnosticService}, unbind from it. */ public void maybeUnbindCallScreeningService() { if (mConnection != null) { Log.i(this, "maybeUnbindCallScreeningService - unbinding from %s", getActiveCallDiagnosticService()); try { mContextProxy.unbindService(mConnection); mCallDiagnosticService = null; mConnection = null; } catch (IllegalArgumentException e) { Log.i(this, "maybeUnbindCallScreeningService: Exception when unbind %s : %s", getActiveCallDiagnosticService(), e.getMessage()); } } else { Log.w(this, "maybeUnbindCallScreeningService - already unbound"); } } /** * Implements the abstracted Telecom functionality the {@link CallDiagnosticServiceAdapter} * depends on. */ private CallDiagnosticServiceAdapter.TelecomAdapter mTelecomAdapter = new CallDiagnosticServiceAdapter.TelecomAdapter() { @Override public void displayDiagnosticMessage(String callId, int messageId, CharSequence message) { handleDisplayDiagnosticMessage(callId, messageId, message); } @Override public void clearDiagnosticMessage(String callId, int messageId) { handleClearDiagnosticMessage(callId, messageId); } @Override public void sendDeviceToDeviceMessage(String callId, @CallDiagnostics.MessageType int message, int value) { handleSendD2DMessage(callId, message, value); } @Override public void overrideDisconnectMessage(String callId, CharSequence message) { handleOverrideDisconnectMessage(callId, message); } }; /** * Sends all calls to the specified {@link CallDiagnosticService}. * @param callDiagnosticService the CDS to send calls to. */ private void handleConnectionComplete(@NonNull ICallDiagnosticService callDiagnosticService) { mAdapter = new CallDiagnosticServiceAdapter(mTelecomAdapter, getActiveCallDiagnosticService(), mLock); try { // Add adapter for communication back from the call diagnostic service to Telecom. callDiagnosticService.setAdapter(mAdapter); // Loop through all the calls we've got ready to send since binding. for (Call call : mCallIdMapper.getCalls()) { sendCallToBoundService(call, callDiagnosticService); } } catch (RemoteException e) { Log.w(this, "handleConnectionComplete: error=%s", e); } } /** * Handles a request from a {@link CallDiagnosticService} to display a diagnostic message. * @param callId the ID of the call to display the message for. * @param message the message. */ private void handleDisplayDiagnosticMessage(@NonNull String callId, int messageId, @Nullable CharSequence message) { Call call = mCallIdMapper.getCall(callId); if (call == null) { Log.w(this, "handleDisplayDiagnosticMessage: callId=%s; msg=%d/%s; invalid call", callId, messageId, message); return; } Log.i(this, "handleDisplayDiagnosticMessage: callId=%s; msg=%d/%s", callId, messageId, message); if (mPlayerFactory != null) { // Play that tone! mPlayerFactory.createPlayer(InCallTonePlayer.TONE_IN_CALL_QUALITY_NOTIFICATION) .startTone(); } call.displayDiagnosticMessage(messageId, message); } /** * Handles a request from a {@link CallDiagnosticService} to clear a previously displayed * diagnostic message. * @param callId the ID of the call to display the message for. * @param messageId the message ID which was previous posted. */ private void handleClearDiagnosticMessage(@NonNull String callId, int messageId) { Call call = mCallIdMapper.getCall(callId); if (call == null) { Log.w(this, "handleClearDiagnosticMessage: callId=%s; msg=%d; invalid call", callId, messageId); return; } Log.i(this, "handleClearDiagnosticMessage: callId=%s; msg=%d; invalid call", callId, messageId); call.clearDiagnosticMessage(messageId); } /** * Handles a request from a {@link CallDiagnosticService} to send a device to device message. * @param callId The ID of the call to send the D2D message for. * @param message The message type. * @param value The message value. */ private void handleSendD2DMessage(@NonNull String callId, @CallDiagnostics.MessageType int message, int value) { Call call = mCallIdMapper.getCall(callId); if (call == null) { Log.w(this, "handleSendD2DMessage: callId=%s; msg=%d/%d; invalid call", callId, message, value); return; } Log.i(this, "handleSendD2DMessage: callId=%s; msg=%d/%d", callId, message, value); call.sendDeviceToDeviceMessage(message, value); } /** * Handles a request from a {@link CallDiagnosticService} to override the disconnect message * for a call. This is the response path from a previous call into the * {@link CallDiagnosticService} via {@link CallDiagnostics#onCallDisconnected(ImsReasonInfo)}. * @param callId The telecom call ID the disconnect override is pending for. * @param message The new disconnect message, or {@code null} if no override. */ private void handleOverrideDisconnectMessage(@NonNull String callId, @Nullable CharSequence message) { Call call = mCallIdMapper.getCall(callId); if (call == null) { Log.w(this, "handleOverrideDisconnectMessage: callId=%s; msg=%s; invalid call", callId, message); return; } Log.i(this, "handleOverrideDisconnectMessage: callId=%s; msg=%s", callId, message); call.handleOverrideDisconnectMessage(message); } /** * Sends a single call to the bound {@link CallDiagnosticService}. * @param call The call to send. * @param callDiagnosticService The CDS to send it to. */ private void sendCallToBoundService(@NonNull Call call, @NonNull ICallDiagnosticService callDiagnosticService) { try { if (isConnected()) { Log.w(this, "sendCallToBoundService: initializing %s", call.getId()); callDiagnosticService.initializeDiagnosticCall(getParceledCall(call)); } else { Log.w(this, "sendCallToBoundService: not bound, skipping %s", call.getId()); } } catch (RemoteException e) { Log.w(this, "sendCallToBoundService: callId=%s, exception=%s", call.getId(), e); } } /** * Removes a call from a bound {@link CallDiagnosticService}. * @param call The call to remove. * @param callDiagnosticService The CDS to remove it from. */ private void removeCallFromBoundService(@NonNull Call call, @NonNull ICallDiagnosticService callDiagnosticService) { try { if (isConnected()) { callDiagnosticService.removeDiagnosticCall(call.getId()); } } catch (RemoteException e) { Log.w(this, "removeCallFromBoundService: callId=%s, exception=%s", call.getId(), e); } } /** * @return {@code true} if the call diagnostic service is bound/connected. */ public boolean isConnected() { return mCallDiagnosticService != null; } /** * Updates the Call diagnostic service with changes to a call. * @param call The updated call. */ private void updateCall(@NonNull Call call) { try { if (isConnected()) { mCallDiagnosticService.updateCall(getParceledCall(call)); } } catch (RemoteException e) { Log.w(this, "updateCall: callId=%s, exception=%s", call.getId(), e); } } /** * Updates the call diagnostic service with a received bluetooth quality report. * @param call The call. * @param report The bluetooth call quality report. */ private void handleBluetoothCallQualityReport(@NonNull Call call, @NonNull BluetoothCallQualityReport report) { try { if (isConnected()) { mCallDiagnosticService.receiveBluetoothCallQualityReport(report); } } catch (RemoteException e) { Log.w(this, "handleBluetoothCallQualityReport: callId=%s, exception=%s", call.getId(), e); } } /** * Informs a CallDiagnosticService of an incoming device to device message which was received * via the carrier network. * @param call the call the message was received via. * @param messageType The message type. * @param messageValue The message value. */ private void handleReceivedDeviceToDeviceMessage(@NonNull Call call, int messageType, int messageValue) { try { if (isConnected()) { mCallDiagnosticService.receiveDeviceToDeviceMessage(call.getId(), messageType, messageValue); } } catch (RemoteException e) { Log.w(this, "handleReceivedDeviceToDeviceMessage: callId=%s, exception=%s", call.getId(), e); } } /** * Handles a reported {@link CallQuality} report from a {@link android.telecom.Connection}. * @param call The call the report originated from. * @param callQualityReport The {@link CallQuality} report. */ private void handleCallQualityReport(@NonNull Call call, @NonNull CallQuality callQualityReport) { try { if (isConnected()) { mCallDiagnosticService.callQualityChanged(call.getId(), callQualityReport); } } catch (RemoteException e) { Log.w(this, "handleCallQualityReport: callId=%s, exception=%s", call.getId(), e); } } /** * Get a parcelled representation of a call for transport to the service. * @param call The call. * @return The parcelled call. */ private @NonNull ParcelableCall getParceledCall(@NonNull Call call) { return ParcelableCallUtils.toParcelableCall( call, false /* includeVideoProvider */, null /* phoneAcctRegistrar */, false /* supportsExternalCalls */, false /* includeRttCall */, false /* isForSystemDialer */ ); } /** * Dumps the state of the {@link CallDiagnosticServiceController}. * * @param pw The {@code IndentingPrintWriter} to write the state to. */ public void dump(IndentingPrintWriter pw) { pw.print("activeCallDiagnosticService: "); pw.println(getActiveCallDiagnosticService()); pw.print("isConnected: "); pw.println(isConnected()); } }