blob: d8ee475e78dcd0cae6742c57b3553f7e28728d0a [file] [log] [blame]
/*
* 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<ResolveInfo> 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<String> 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<ResolveInfo> 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());
}
}