blob: ad3d26a66df436f820053e7d4fb0010f03dd9f4b [file] [log] [blame]
/*
* Copyright (C) 2014 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 static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
import static android.os.Process.myUid;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.AttributionSource;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.PermissionChecker;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.hardware.SensorPrivacyManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PackageTagsList;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
import android.telecom.ConnectionService;
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.ParcelableCall;
import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.telecom.IInCallService;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.SystemStateHelper.SystemStateListener;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.ui.NotificationChannelManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
* can send updates to the in-call app. This class is created and owned by CallsManager and retains
* a binding to the {@link IInCallService} (implemented by the in-call app).
*/
public class InCallController extends CallsManagerListenerBase implements
AppOpsManager.OnOpActiveChangedListener {
public static final String NOTIFICATION_TAG = InCallController.class.getSimpleName();
public static final int IN_CALL_SERVICE_NOTIFICATION_ID = 3;
private AnomalyReporterAdapter mAnomalyReporter = new AnomalyReporterAdapterImpl();
/**
* Anomaly Report UUIDs and corresponding error descriptions specific to InCallController.
*/
public static final UUID SET_IN_CALL_ADAPTER_ERROR_UUID =
UUID.fromString("0c2adf96-353a-433c-afe9-1e5564f304f9");
public static final String SET_IN_CALL_ADAPTER_ERROR_MSG =
"Exception thrown while setting the in-call adapter.";
@VisibleForTesting
public void setAnomalyReporterAdapter(AnomalyReporterAdapter mAnomalyReporterAdapter){
mAnomalyReporter = mAnomalyReporterAdapter;
}
public class InCallServiceConnection {
/**
* Indicates that a call to {@link #connect(Call)} has succeeded and resulted in a
* connection to an InCallService.
*/
public static final int CONNECTION_SUCCEEDED = 1;
/**
* Indicates that a call to {@link #connect(Call)} has failed because of a binding issue.
*/
public static final int CONNECTION_FAILED = 2;
/**
* Indicates that a call to {@link #connect(Call)} has been skipped because the
* IncallService does not support the type of call..
*/
public static final int CONNECTION_NOT_SUPPORTED = 3;
public class Listener {
public void onDisconnect(InCallServiceConnection conn, Call call) {}
}
protected Listener mListener;
public int connect(Call call) { return CONNECTION_FAILED; }
public void disconnect() {}
public boolean isConnected() { return false; }
public void setHasEmergency(boolean hasEmergency) {}
public void setListener(Listener l) {
mListener = l;
}
public InCallServiceInfo getInfo() { return null; }
public void dump(IndentingPrintWriter pw) {}
public Call mCall;
}
public static class InCallServiceInfo {
private final ComponentName mComponentName;
private boolean mIsExternalCallsSupported;
private boolean mIsSelfManagedCallsSupported;
private final int mType;
private long mBindingStartTime;
private long mDisconnectTime;
private boolean mHasCrossUserOrProfilePerm;
public InCallServiceInfo(ComponentName componentName,
boolean isExternalCallsSupported,
boolean isSelfManageCallsSupported,
int type, boolean hasCrossUserOrProfilePerm) {
mComponentName = componentName;
mIsExternalCallsSupported = isExternalCallsSupported;
mIsSelfManagedCallsSupported = isSelfManageCallsSupported;
mType = type;
mHasCrossUserOrProfilePerm = hasCrossUserOrProfilePerm;
}
public boolean hasCrossUserOrProfilePermission() { return mHasCrossUserOrProfilePerm; }
public ComponentName getComponentName() {
return mComponentName;
}
public boolean isExternalCallsSupported() {
return mIsExternalCallsSupported;
}
public boolean isSelfManagedCallsSupported() {
return mIsSelfManagedCallsSupported;
}
public int getType() {
return mType;
}
public long getBindingStartTime() {
return mBindingStartTime;
}
public long getDisconnectTime() {
return mDisconnectTime;
}
public void setBindingStartTime(long bindingStartTime) {
mBindingStartTime = bindingStartTime;
}
public void setDisconnectTime(long disconnectTime) {
mDisconnectTime = disconnectTime;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
InCallServiceInfo that = (InCallServiceInfo) o;
if (mIsExternalCallsSupported != that.mIsExternalCallsSupported) {
return false;
}
if (mIsSelfManagedCallsSupported != that.mIsSelfManagedCallsSupported) {
return false;
}
return mComponentName.equals(that.mComponentName);
}
@Override
public int hashCode() {
return Objects.hash(mComponentName, mIsExternalCallsSupported,
mIsSelfManagedCallsSupported);
}
@Override
public String toString() {
return "[" + mComponentName + " supportsExternal? " + mIsExternalCallsSupported +
" supportsSelfMg?" + mIsSelfManagedCallsSupported + "]";
}
}
private class InCallServiceBindingConnection extends InCallServiceConnection {
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.startSession("ICSBC.oSC", Log.getPackageAbbreviation(name));
synchronized (mLock) {
try {
Log.d(this, "onServiceConnected: %s %b %b", name, mIsBound, mIsConnected);
mIsBound = true;
if (mIsConnected) {
// Only proceed if we are supposed to be connected.
onConnected(service);
}
} finally {
Log.endSession();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.startSession("ICSBC.oSD", Log.getPackageAbbreviation(name));
synchronized (mLock) {
try {
Log.d(this, "onServiceDisconnected: %s", name);
mIsBound = false;
onDisconnected();
} finally {
Log.endSession();
}
}
}
@Override
public void onNullBinding(ComponentName name) {
Log.startSession("ICSBC.oNB", Log.getPackageAbbreviation(name));
synchronized (mLock) {
try {
Log.d(this, "onNullBinding: %s", name);
mIsNullBinding = true;
mIsBound = false;
onDisconnected();
} finally {
Log.endSession();
}
}
}
@Override
public void onBindingDied(ComponentName name) {
Log.startSession("ICSBC.oBD", Log.getPackageAbbreviation(name));
synchronized (mLock) {
try {
Log.d(this, "onBindingDied: %s", name);
mIsBound = false;
onDisconnected();
} finally {
Log.endSession();
}
}
}
};
private final InCallServiceInfo mInCallServiceInfo;
private boolean mIsConnected = false;
private boolean mIsBound = false;
private boolean mIsNullBinding = false;
private NotificationManager mNotificationManager;
//this is really used for cases where the userhandle for a call
//does not match what we want to use for bindAsUser
private final UserHandle mUserHandleToUseForBinding;
public InCallServiceBindingConnection(InCallServiceInfo info) {
mInCallServiceInfo = info;
mUserHandleToUseForBinding = null;
}
public InCallServiceBindingConnection(InCallServiceInfo info,
UserHandle userHandleToUseForBinding) {
mInCallServiceInfo = info;
mUserHandleToUseForBinding = userHandleToUseForBinding;
}
@Override
public int connect(Call call) {
UserHandle userFromCall = getUserFromCall(call);
if (mIsConnected) {
Log.addEvent(call, LogUtils.Events.INFO, "Already connected, ignoring request: "
+ mInCallServiceInfo);
if (call != null) {
// Track the call if we don't already know about it.
addCall(call);
// Notify this new added call
sendCallToService(call, mInCallServiceInfo,
mInCallServices.get(userFromCall).get(mInCallServiceInfo));
}
return CONNECTION_SUCCEEDED;
}
if (call != null && call.isSelfManaged() &&
(!mInCallServiceInfo.isSelfManagedCallsSupported()
|| !call.visibleToInCallService())) {
Log.i(this, "Skipping binding to %s - doesn't support self-mgd calls",
mInCallServiceInfo);
mIsConnected = false;
return CONNECTION_NOT_SUPPORTED;
}
Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
intent.setComponent(mInCallServiceInfo.getComponentName());
if (call != null && !call.isIncoming() && !call.isExternalCall()) {
intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
call.getIntentExtras());
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
call.getTargetPhoneAccount());
}
Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
mIsConnected = true;
mInCallServiceInfo.setBindingStartTime(mClockProxy.elapsedRealtime());
boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userFromCall);
// Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
// it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
// active, we know that a work sim was previously used to place a MO emergency call. We
// need to ensure that we bind to the CURRENT_USER in this case, as the work user would
// not be running (handled in getUserFromCall).
UserHandle userToBind = isManagedProfile ? userFromCall : UserHandle.CURRENT;
if ((mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_NON_UI
|| mInCallServiceInfo.mType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI) && (
mUserHandleToUseForBinding != null)) {
//guarding change for non-UI/carmode-UI services which may not be present for
// work profile.
//In this case, we use the parent user handle. (This also seems to be more
// accurate that USER_CURRENT since we queried/discovered the packages using the
// parent handle)
if (mInCallServiceInfo.hasCrossUserOrProfilePermission()) {
userToBind = mUserHandleToUseForBinding;
} else {
Log.i(this,
"service does not have INTERACT_ACROSS_PROFILES or "
+ "INTERACT_ACROSS_USERS permission");
}
}
Log.i(this, "using user id: %s for binding. User from Call is: %s", userToBind,
userFromCall);
if (!mContext.bindServiceAsUser(intent, mServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
| Context.BIND_SCHEDULE_LIKE_TOP_APP, userToBind)) {
Log.w(this, "Failed to connect.");
mIsConnected = false;
}
if (mIsConnected && call != null) {
mCall = call;
}
Log.i(this, "mCall: %s, mIsConnected: %s", mCall, mIsConnected);
return mIsConnected ? CONNECTION_SUCCEEDED : CONNECTION_FAILED;
}
@Override
public InCallServiceInfo getInfo() {
return mInCallServiceInfo;
}
@Override
public void disconnect() {
if (mIsConnected) {
UserHandle userFromCall = getUserFromCall(mCall);
mInCallServiceInfo.setDisconnectTime(mClockProxy.elapsedRealtime());
Log.i(InCallController.this, "ICSBC#disconnect: unbinding after %s ms;"
+ "%s. isCrashed: %s", mInCallServiceInfo.mDisconnectTime
- mInCallServiceInfo.mBindingStartTime,
mInCallServiceInfo, mIsNullBinding);
String packageName = mInCallServiceInfo.getComponentName().getPackageName();
mContext.unbindService(mServiceConnection);
mIsConnected = false;
if (mIsNullBinding && mInCallServiceInfo.getType() != IN_CALL_SERVICE_TYPE_NON_UI) {
// Non-UI InCallServices are allowed to return null from onBind if they don't
// want to handle calls at the moment, so don't report them to the user as
// crashed.
sendCrashedInCallServiceNotification(packageName, userFromCall);
}
if (mCall != null) {
mCall.getAnalytics().addInCallService(
mInCallServiceInfo.getComponentName().flattenToShortString(),
mInCallServiceInfo.getType(),
mInCallServiceInfo.getDisconnectTime()
- mInCallServiceInfo.getBindingStartTime(), mIsNullBinding);
updateCallTracking(mCall, mInCallServiceInfo, false /* isAdd */);
}
InCallController.this.onDisconnected(mInCallServiceInfo, userFromCall);
} else {
Log.i(InCallController.this, "ICSBC#disconnect: already disconnected; %s",
mInCallServiceInfo);
Log.addEvent(null, LogUtils.Events.INFO, "Already disconnected, ignoring request.");
}
}
@Override
public boolean isConnected() {
return mIsConnected;
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.print("BindingConnection [");
pw.print(mIsConnected ? "" : "not ");
pw.print("connected, ");
pw.print(mIsBound ? "" : "not ");
pw.print("bound, ");
pw.print(mInCallServiceInfo);
pw.println("\n");
}
protected void onConnected(IBinder service) {
boolean shouldRemainConnected =
InCallController.this.onConnected(mInCallServiceInfo, service,
getUserFromCall(mCall));
if (!shouldRemainConnected) {
// Sometimes we can opt to disconnect for certain reasons, like if the
// InCallService rejected our initialization step, or the calls went away
// in the time it took us to bind to the InCallService. In such cases, we go
// ahead and disconnect ourselves.
disconnect();
}
}
protected void onDisconnected() {
boolean shouldReconnect = mIsConnected;
InCallController.this.onDisconnected(mInCallServiceInfo, getUserFromCall(mCall));
disconnect(); // Unbind explicitly if we get disconnected.
if (mListener != null) {
mListener.onDisconnect(InCallServiceBindingConnection.this, mCall);
}
// Check if we are expected to reconnect
if (shouldReconnect && shouldHandleReconnect()) {
connect(mCall); // reconnect
}
}
private boolean shouldHandleReconnect() {
int serviceType = mInCallServiceInfo.getType();
boolean nonUI = (serviceType == IN_CALL_SERVICE_TYPE_NON_UI)
|| (serviceType == IN_CALL_SERVICE_TYPE_COMPANION);
boolean carModeUI = (serviceType == IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
return carModeUI || (nonUI && !mIsNullBinding);
}
}
/**
* A version of the InCallServiceBindingConnection that proxies all calls to a secondary
* connection until it finds an emergency call, or the other connection dies. When one of those
* two things happen, this class instance will take over the connection.
*/
private class EmergencyInCallServiceConnection extends InCallServiceBindingConnection {
private boolean mIsProxying = true;
private boolean mIsConnected = false;
private final InCallServiceConnection mSubConnection;
private Listener mSubListener = new Listener() {
@Override
public void onDisconnect(InCallServiceConnection subConnection, Call call) {
if (subConnection == mSubConnection) {
if (mIsConnected && mIsProxying) {
// At this point we know that we need to be connected to the InCallService
// and we are proxying to the sub connection. However, the sub-connection
// just died so we need to stop proxying and connect to the system in-call
// service instead.
mIsProxying = false;
connect(call);
}
}
}
};
public EmergencyInCallServiceConnection(
InCallServiceInfo info, InCallServiceConnection subConnection) {
super(info);
mSubConnection = subConnection;
if (mSubConnection != null) {
mSubConnection.setListener(mSubListener);
}
mIsProxying = (mSubConnection != null);
}
@Override
public int connect(Call call) {
mIsConnected = true;
if (mIsProxying) {
int result = mSubConnection.connect(call);
mIsConnected = result == CONNECTION_SUCCEEDED;
if (result != CONNECTION_FAILED) {
return result;
}
// Could not connect to child, stop proxying.
mIsProxying = false;
}
UserHandle userFromCall = getUserFromCall(call);
mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
userFromCall);
if (call != null && call.isIncoming()
&& mEmergencyCallHelper.getLastEmergencyCallTimeMillis() > 0) {
// Add the last emergency call time to the call
Bundle extras = new Bundle();
extras.putLong(android.telecom.Call.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS,
mEmergencyCallHelper.getLastEmergencyCallTimeMillis());
call.putConnectionServiceExtras(extras);
}
// If we are here, we didn't or could not connect to child. So lets connect ourselves.
return super.connect(call);
}
@Override
public void disconnect() {
Log.i(this, "Disconnecting from InCallService");
if (mIsProxying) {
mSubConnection.disconnect();
} else {
super.disconnect();
mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
}
mIsConnected = false;
}
@Override
public void setHasEmergency(boolean hasEmergency) {
if (hasEmergency) {
takeControl();
}
}
@Override
public InCallServiceInfo getInfo() {
if (mIsProxying) {
return mSubConnection.getInfo();
} else {
return super.getInfo();
}
}
@Override
protected void onDisconnected() {
// Save this here because super.onDisconnected() could force us to explicitly
// disconnect() as a cleanup step and that sets mIsConnected to false.
boolean shouldReconnect = mIsConnected;
super.onDisconnected();
// We just disconnected. Check if we are expected to be connected, and reconnect.
if (shouldReconnect && !mIsProxying) {
connect(mCall); // reconnect
}
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.print("Emergency ICS Connection [");
pw.append(mIsProxying ? "" : "not ").append("proxying, ");
pw.append(mIsConnected ? "" : "not ").append("connected]\n");
pw.increaseIndent();
pw.print("Emergency: ");
super.dump(pw);
if (mSubConnection != null) {
pw.print("Default-Dialer: ");
mSubConnection.dump(pw);
}
pw.decreaseIndent();
}
/**
* Forces the connection to take control from it's subConnection.
*/
private void takeControl() {
if (mIsProxying) {
mIsProxying = false;
if (mIsConnected) {
mSubConnection.disconnect();
super.connect(null);
}
}
}
}
/**
* A version of InCallServiceConnection which switches UI between two separate sub-instances of
* InCallServicesConnections.
*/
private class CarSwappingInCallServiceConnection extends InCallServiceConnection {
private final InCallServiceConnection mDialerConnection;
private InCallServiceConnection mCarModeConnection;
private InCallServiceConnection mCurrentConnection;
private boolean mIsCarMode = false;
private boolean mIsConnected = false;
public CarSwappingInCallServiceConnection(
InCallServiceConnection dialerConnection,
InCallServiceConnection carModeConnection) {
mDialerConnection = dialerConnection;
mCarModeConnection = carModeConnection;
mCurrentConnection = getCurrentConnection();
}
/**
* Called when we move to a state where calls are present on the device. Chooses the
* {@link InCallService} to which we should connect.
*
* @param isCarMode {@code true} if device is in car mode, {@code false} otherwise.
*/
public synchronized void chooseInitialInCallService(boolean isCarMode) {
Log.i(this, "chooseInitialInCallService: " + mIsCarMode + " => " + isCarMode);
if (isCarMode != mIsCarMode) {
mIsCarMode = isCarMode;
InCallServiceConnection newConnection = getCurrentConnection();
if (newConnection != mCurrentConnection) {
if (mIsConnected) {
mCurrentConnection.disconnect();
}
int result = newConnection.connect(null);
mIsConnected = result == CONNECTION_SUCCEEDED;
mCurrentConnection = newConnection;
}
}
}
/**
* Invoked when {@link CarModeTracker} has determined that the device is no longer in car
* mode (i.e. has no car mode {@link InCallService}).
*
* Switches back to the default dialer app.
*/
public synchronized void disableCarMode() {
mIsCarMode = false;
if (mIsConnected) {
mCurrentConnection.disconnect();
}
mCurrentConnection = mDialerConnection;
int result = mDialerConnection.connect(null);
mIsConnected = result == CONNECTION_SUCCEEDED;
}
/**
* Changes the active {@link InCallService} to a car mode app. Called whenever the device
* changes to car mode or the currently active car mode app changes.
*
* @param packageName The package name of the car mode app.
*/
public synchronized void changeCarModeApp(String packageName, UserHandle userHandle) {
Log.i(this, "changeCarModeApp: isCarModeNow=" + mIsCarMode);
InCallServiceInfo currentConnectionInfo = mCurrentConnection == null ? null
: mCurrentConnection.getInfo();
InCallServiceInfo carModeConnectionInfo =
getInCallServiceComponent(userHandle, packageName,
IN_CALL_SERVICE_TYPE_CAR_MODE_UI, true /* ignoreDisabed */);
if (!Objects.equals(currentConnectionInfo, carModeConnectionInfo)
&& carModeConnectionInfo != null) {
Log.i(this, "changeCarModeApp: " + currentConnectionInfo + " => "
+ carModeConnectionInfo);
if (mIsConnected) {
mCurrentConnection.disconnect();
}
mCarModeConnection = mCurrentConnection =
new InCallServiceBindingConnection(carModeConnectionInfo, userHandle);
mIsCarMode = true;
int result = mCurrentConnection.connect(null);
mIsConnected = result == CONNECTION_SUCCEEDED;
} else {
Log.i(this, "changeCarModeApp: unchanged; " + currentConnectionInfo + " => "
+ carModeConnectionInfo);
}
}
public boolean isCarMode() {
return mIsCarMode;
}
@Override
public int connect(Call call) {
if (mIsConnected) {
Log.i(this, "already connected");
return CONNECTION_SUCCEEDED;
} else {
int result = mCurrentConnection.connect(call);
if (result != CONNECTION_FAILED) {
mIsConnected = result == CONNECTION_SUCCEEDED;
return result;
}
}
return CONNECTION_FAILED;
}
@Override
public void disconnect() {
if (mIsConnected) {
Log.i(InCallController.this, "CSICSC: disconnect %s", mCurrentConnection);
mCurrentConnection.disconnect();
mIsConnected = false;
} else {
Log.i(this, "already disconnected");
}
}
@Override
public boolean isConnected() {
return mIsConnected;
}
@Override
public void setHasEmergency(boolean hasEmergency) {
if (mDialerConnection != null) {
mDialerConnection.setHasEmergency(hasEmergency);
}
if (mCarModeConnection != null) {
mCarModeConnection.setHasEmergency(hasEmergency);
}
}
@Override
public InCallServiceInfo getInfo() {
return mCurrentConnection.getInfo();
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.print("Car Swapping ICS [");
pw.append(mIsConnected ? "" : "not ").append("connected]\n");
pw.increaseIndent();
if (mDialerConnection != null) {
pw.print("Dialer: ");
mDialerConnection.dump(pw);
}
if (mCarModeConnection != null) {
pw.print("Car Mode: ");
mCarModeConnection.dump(pw);
}
}
private InCallServiceConnection getCurrentConnection() {
if (mIsCarMode && mCarModeConnection != null) {
return mCarModeConnection;
} else {
return mDialerConnection;
}
}
}
private class NonUIInCallServiceConnectionCollection extends InCallServiceConnection {
private final List<InCallServiceBindingConnection> mSubConnections;
public NonUIInCallServiceConnectionCollection(
List<InCallServiceBindingConnection> subConnections) {
mSubConnections = subConnections;
}
@Override
public int connect(Call call) {
for (InCallServiceBindingConnection subConnection : mSubConnections) {
subConnection.connect(call);
}
return CONNECTION_SUCCEEDED;
}
@Override
public void disconnect() {
for (InCallServiceBindingConnection subConnection : mSubConnections) {
if (subConnection.isConnected()) {
subConnection.disconnect();
}
}
}
@Override
public boolean isConnected() {
boolean connected = false;
for (InCallServiceBindingConnection subConnection : mSubConnections) {
connected = connected || subConnection.isConnected();
}
return connected;
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.println("Non-UI Connections:");
pw.increaseIndent();
for (InCallServiceBindingConnection subConnection : mSubConnections) {
subConnection.dump(pw);
}
pw.decreaseIndent();
}
public void addConnections(List<InCallServiceBindingConnection> newConnections) {
// connect() needs to be called with a Call object. Since we're in the middle of any
// possible number of calls right now, choose an arbitrary one from the ones that
// InCallController is tracking.
if (mCallIdMapper.getCalls().isEmpty()) {
Log.w(InCallController.this, "No calls tracked while adding new NonUi incall");
return;
}
Call callToConnectWith = mCallIdMapper.getCalls().iterator().next();
for (InCallServiceBindingConnection newConnection : newConnections) {
// Ensure we track the new sub-connection so that when we later disconnect we will
// be able to disconnect it.
mSubConnections.add(newConnection);
newConnection.connect(callToConnectWith);
}
}
public List<InCallServiceBindingConnection> getSubConnections() {
return mSubConnections;
}
}
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, false /* includeVideoProvider */, didRttChange, null);
}
@Override
public void onCannedSmsResponsesLoaded(Call call) {
updateCall(call);
}
@Override
public void onVideoCallProviderChanged(Call call) {
updateCall(call, true /* videoProviderChanged */, false, null);
}
@Override
public void onStatusHintsChanged(Call call) {
updateCall(call);
}
@Override
public void onCallerInfoChanged(Call call) {
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,
String requestingPackageName) {
if (source == Call.SOURCE_CONNECTION_SERVICE) {
updateCall(call);
} else if (source == Call.SOURCE_INCALL_SERVICE && requestingPackageName != null) {
// If the change originated from another InCallService, we'll propagate the change
// to all other InCallServices running, EXCEPT the one who made the original change.
updateCall(call, false /* videoProviderChanged */, false /* rttInfoChanged */,
requestingPackageName);
}
}
/**
* 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);
}
@Override
public void onHandleChanged(Call call) {
updateCall(call);
}
@Override
public void onCallerDisplayNameChanged(Call call) {
updateCall(call);
}
@Override
public void onCallDirectionChanged(Call call) {
updateCall(call);
}
@Override
public void onVideoStateChanged(Call call, int previousVideoState, int newVideoState) {
updateCall(call);
}
@Override
public void onTargetPhoneAccountChanged(Call call) {
updateCall(call);
}
@Override
public void onConferenceableCallsChanged(Call call) {
updateCall(call);
}
@Override
public void onConnectionEvent(Call call, String event, Bundle extras) {
notifyConnectionEvent(call, event, extras);
}
@Override
public void onHandoverFailed(Call call, int error) {
notifyHandoverFailed(call, error);
}
@Override
public void onHandoverComplete(Call call) {
notifyHandoverComplete(call);
}
@Override
public void onRttInitiationFailure(Call call, int reason) {
notifyRttInitiationFailure(call, reason);
updateCall(call, false, true, null);
}
@Override
public void onRemoteRttRequest(Call call, int requestId) {
notifyRemoteRttRequest(call, requestId);
}
@Override
public void onCallerNumberVerificationStatusChanged(Call call,
int callerNumberVerificationStatus) {
updateCall(call);
}
};
private UserHandle findChildManagedProfileUser(UserHandle parent, UserManager um) {
UserHandle childManagedProfileUser = null;
//find child managed profile user (if any)
List<UserHandle> allUsers = um.getAllProfiles();
for (UserHandle u : allUsers) {
if ((um.getProfileParent(u) != null) && (um.getProfileParent(u).equals(parent))
&& um.isManagedProfile(u.getIdentifier())) {
//found managed profile child
Log.i(this,
"Child managed profile user found: " + u.getIdentifier());
childManagedProfileUser = u;
break;
}
}
return childManagedProfileUser;
}
private BroadcastReceiver mPackageChangedReceiver = new BroadcastReceiver() {
private List<InCallController.InCallServiceInfo> getNonUiInCallServiceInfoList(
Intent intent, UserHandle userHandle) {
String changedPackage = intent.getData().getSchemeSpecificPart();
List<InCallController.InCallServiceInfo> inCallServiceInfoList =
Arrays.stream(intent.getStringArrayExtra(
Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST))
.map((className) ->
ComponentName.createRelative(changedPackage,
className))
.filter(mKnownNonUiInCallServices::contains)
.flatMap(componentName -> getInCallServiceComponents(
userHandle, componentName,
IN_CALL_SERVICE_TYPE_NON_UI).stream())
.collect(Collectors.toList());
return ((inCallServiceInfoList != null) ? inCallServiceInfoList : new ArrayList<>());
}
//Here we query components using the userHandle. We then also query components using the
//parent userHandle (if any) while removing duplicates. For non-dup components found using
//parent userHandle, we use the overloaded InCallServiceBindingConnection constructor.
@SuppressWarnings("ReturnValueIgnored")
private List<InCallServiceBindingConnection> getNonUiInCallServiceBindingConnectionList(
Intent intent, @NonNull UserHandle userHandle, UserHandle parentUserHandle) {
List<InCallServiceBindingConnection> result = new ArrayList<>();
List<InCallController.InCallServiceInfo> serviceInfoListForParent = new ArrayList<>();
//query and add components for the child
List<InCallController.InCallServiceInfo> serviceInfoListForUser =
getNonUiInCallServiceInfoList(intent, userHandle);
//if user has a parent, get components for parents
if (parentUserHandle != null) {
serviceInfoListForParent = getNonUiInCallServiceInfoList(intent, parentUserHandle);
}
serviceInfoListForUser
.stream()
.map(InCallServiceBindingConnection::new)
.collect(Collectors.toCollection(() -> result));
serviceInfoListForParent
.stream()
.filter((e) -> !(serviceInfoListForUser.contains(e)))
.map((serviceinfo) -> new InCallServiceBindingConnection(serviceinfo,
parentUserHandle))
.collect(Collectors.toCollection(() -> result));
return result;
}
@Override
public void onReceive(Context context, Intent intent) {
Log.startSession("ICC.pCR");
UserManager um = mContext.getSystemService(UserManager.class);
try {
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
synchronized (mLock) {
int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
boolean isManagedProfile = um.isManagedProfile(userHandle.getIdentifier());
/*
There are two possibilities here:
1) We get a work-profile/managed userHandle. In this case we need to check
if there are any ongoing calls for that user. If yes, then process further
by querying component using this user handle (also bindAsUser using this
handle). Else safely ignore it.
OR
2) We get the primary/non-managed userHandle. In this case, we have two
sub-cases to handle:
a) If there are ongoing calls for this user, query components
using this user and addConnections
b) If there are ongoing calls for the child of this user, we
also addConnections to that child (but invoke bindAsUser later
with the parent handle).
*/
UserHandle childManagedProfileUser = findChildManagedProfileUser(
userHandle, um);
boolean isUserKeyPresent = mNonUIInCallServiceConnections.containsKey(
userHandle);
boolean isChildUserKeyPresent = (childManagedProfileUser == null) ? false
: mNonUIInCallServiceConnections.containsKey(
childManagedProfileUser);
List<InCallServiceBindingConnection> componentsToBindForUser = null;
List<InCallServiceBindingConnection> componentsToBindForChild = null;
if(isUserKeyPresent) {
componentsToBindForUser =
getNonUiInCallServiceBindingConnectionList(intent,
userHandle, null);
}
if (isChildUserKeyPresent) {
componentsToBindForChild =
getNonUiInCallServiceBindingConnectionList(intent,
childManagedProfileUser, userHandle);
}
Log.i(this,
"isUserKeyPresent:%b isChildKeyPresent:%b isManagedProfile:%b "
+ "user:%d",
isUserKeyPresent, isChildUserKeyPresent, isManagedProfile,
userHandle.getIdentifier());
if (isUserKeyPresent && componentsToBindForUser != null) {
mNonUIInCallServiceConnections.get(userHandle).
addConnections(componentsToBindForUser);
}
if (isChildUserKeyPresent && componentsToBindForChild != null) {
mNonUIInCallServiceConnections.get(childManagedProfileUser).
addConnections(componentsToBindForChild);
}
// If the current car mode app become enabled from disabled, update
// the connection to binding
updateCarModeForConnections();
}
}
} finally {
Log.endSession();
}
}
};
private final BroadcastReceiver mUserAddedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) {
restrictPhoneCallOps();
}
}
};
private final SystemStateListener mSystemStateListener = new SystemStateListener() {
@Override
public void onCarModeChanged(int priority, String packageName, boolean isCarMode) {
InCallController.this.handleCarModeChange(priority, packageName, isCarMode);
}
@Override
public void onAutomotiveProjectionStateSet(String automotiveProjectionPackage) {
InCallController.this.handleSetAutomotiveProjection(automotiveProjectionPackage);
}
@Override
public void onAutomotiveProjectionStateReleased() {
InCallController.this.handleReleaseAutomotiveProjection();
}
@Override
public void onPackageUninstalled(String packageName) {
mCarModeTracker.forceRemove(packageName);
updateCarModeForConnections();
}
};
private static final int IN_CALL_SERVICE_TYPE_INVALID = 0;
private static final int IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI = 1;
private static final int IN_CALL_SERVICE_TYPE_SYSTEM_UI = 2;
private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
private static final int IN_CALL_SERVICE_TYPE_COMPANION = 5;
private static final int[] LIVE_CALL_STATES = { CallState.ACTIVE, CallState.PULLING,
CallState.DISCONNECTING };
/** The in-call app implementations, see {@link IInCallService}. */
private final Map<UserHandle, Map<InCallServiceInfo, IInCallService>>
mInCallServices = new ArrayMap<>();
private final CallIdMapper mCallIdMapper = new CallIdMapper(Call::getId);
private final Context mContext;
private final AppOpsManager mAppOpsManager;
private final SensorPrivacyManager mSensorPrivacyManager;
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
private final SystemStateHelper mSystemStateHelper;
private final Timeouts.Adapter mTimeoutsAdapter;
private final DefaultDialerCache mDefaultDialerCache;
private final EmergencyCallHelper mEmergencyCallHelper;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Map<UserHandle, CarSwappingInCallServiceConnection>
mInCallServiceConnections = new ArrayMap<>();
private final Map<UserHandle, NonUIInCallServiceConnectionCollection>
mNonUIInCallServiceConnections = new ArrayMap<>();
private final ClockProxy mClockProxy;
private final IBinder mToken = new Binder();
// A set of known non-UI in call services on the device, including those that are disabled.
// We track this so that we can efficiently bind to them when we're notified that a new
// component has been enabled.
private Set<ComponentName> mKnownNonUiInCallServices = new ArraySet<>();
// Future that's in a completed state unless we're in the middle of binding to a service.
// The future will complete with true if binding succeeds, false if it timed out.
private CompletableFuture<Boolean> mBindingFuture = CompletableFuture.completedFuture(true);
private final CarModeTracker mCarModeTracker;
/**
* The package name of the app which is showing the calling UX.
*/
private String mCurrentUserInterfacePackageName = null;
/**
* {@code true} if InCallController is tracking a managed, not external call which is using the
* microphone, and is not muted {@code false} otherwise.
*/
private boolean mIsCallUsingMicrophone = false;
/**
* {@code true} if InCallController is tracking a managed, not external call which is using the
* microphone, {@code false} otherwise.
*/
private boolean mIsTrackingManagedAliveCall = false;
private boolean mIsStartCallDelayScheduled = false;
/**
* A list of call IDs which are currently using the camera.
*/
private ArrayList<String> mCallsUsingCamera = new ArrayList<>();
private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>();
private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>();
private FeatureFlags mFeatureFlags;
public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
CarModeTracker carModeTracker, ClockProxy clockProxy, FeatureFlags featureFlags) {
mContext = context;
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
mLock = lock;
mCallsManager = callsManager;
mSystemStateHelper = systemStateHelper;
mTimeoutsAdapter = timeoutsAdapter;
mDefaultDialerCache = defaultDialerCache;
mEmergencyCallHelper = emergencyCallHelper;
mCarModeTracker = carModeTracker;
mSystemStateHelper.addListener(mSystemStateListener);
mClockProxy = clockProxy;
restrictPhoneCallOps();
IntentFilter userAddedFilter = new IntentFilter(Intent.ACTION_USER_ADDED);
userAddedFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiver(mUserAddedReceiver, userAddedFilter);
mFeatureFlags = featureFlags;
}
private void restrictPhoneCallOps() {
PackageTagsList packageRestriction = new PackageTagsList.Builder()
.add(mContext.getPackageName())
.build();
mAppOpsManager.setUserRestrictionForUser(AppOpsManager.OP_PHONE_CALL_MICROPHONE, true,
mToken, packageRestriction, UserHandle.USER_ALL);
mAppOpsManager.setUserRestrictionForUser(AppOpsManager.OP_PHONE_CALL_CAMERA, true,
mToken, packageRestriction, UserHandle.USER_ALL);
}
@Override
public void onOpActiveChanged(@androidx.annotation.NonNull String op, int uid,
@androidx.annotation.NonNull String packageName, boolean active) {
synchronized (mLock) {
if (!mAllCarrierPrivilegedApps.contains(packageName)) {
return;
}
if (active) {
mActiveCarrierPrivilegedApps.add(packageName);
} else {
mActiveCarrierPrivilegedApps.remove(packageName);
}
maybeTrackMicrophoneUse(isMuted());
}
}
private void updateAllCarrierPrivilegedUsingMic() {
mActiveCarrierPrivilegedApps.clear();
UserManager userManager = mContext.getSystemService(UserManager.class);
PackageManager pkgManager = mContext.getPackageManager();
for (String pkg : mAllCarrierPrivilegedApps) {
boolean isActive = mActiveCarrierPrivilegedApps.contains(pkg);
List<UserHandle> users = userManager.getUserHandles(true);
for (UserHandle user : users) {
if (isActive) {
break;
}
int uid;
try {
uid = pkgManager.getPackageUidAsUser(pkg, user.getIdentifier());
} catch (PackageManager.NameNotFoundException e) {
continue;
}
List<AppOpsManager.PackageOps> pkgOps = mAppOpsManager.getOpsForPackage(
uid, pkg, OPSTR_RECORD_AUDIO);
for (int j = 0; j < pkgOps.size(); j++) {
List<AppOpsManager.OpEntry> opEntries = pkgOps.get(j).getOps();
for (int k = 0; k < opEntries.size(); k++) {
AppOpsManager.OpEntry entry = opEntries.get(k);
if (entry.isRunning()) {
mActiveCarrierPrivilegedApps.add(pkg);
break;
}
}
}
}
}
}
private void updateAllCarrierPrivileged() {
mAllCarrierPrivilegedApps.clear();
for (Call call : mCallIdMapper.getCalls()) {
mAllCarrierPrivilegedApps.add(call.getConnectionManagerPhoneAccount()
.getComponentName().getPackageName());
}
}
@Override
public void onCallAdded(Call call) {
UserHandle userFromCall = getUserFromCall(call);
Log.i(this, "onCallAdded: %s", call);
// Track the call if we don't already know about it.
addCall(call);
if (!isBoundAndConnectedToServices(userFromCall)) {
Log.i(this, "onCallAdded: %s; not bound or connected.", call);
// We are not bound, or we're not connected.
bindToServices(call);
} else {
InCallServiceConnection inCallServiceConnection =
mInCallServiceConnections.get(userFromCall);
// We are bound, and we are connected.
adjustServiceBindingsForEmergency(userFromCall);
// This is in case an emergency call is added while there is an existing call.
mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(call,
userFromCall);
if (inCallServiceConnection != null) {
Log.i(this, "mInCallServiceConnection isConnected=%b",
inCallServiceConnection.isConnected());
}
List<ComponentName> componentsUpdated = new ArrayList<>();
if (mInCallServices.containsKey(userFromCall)) {
for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
if (call.isExternalCall() && !info.isExternalCallsSupported()) {
continue;
}
if (call.isSelfManaged() && (!call.visibleToInCallService()
|| !info.isSelfManagedCallsSupported())) {
continue;
}
// Only send the RTT call if it's a UI in-call service
boolean includeRttCall = false;
if (inCallServiceConnection != null) {
includeRttCall = info.equals(inCallServiceConnection.getInfo());
}
componentsUpdated.add(info.getComponentName());
IInCallService inCallService = entry.getValue();
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
true /* includeVideoProvider */,
mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(), includeRttCall,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
try {
inCallService.addCall(
sanitizeParcelableCallForService(info, parcelableCall));
updateCallTracking(call, info, true /* isAdd */);
} catch (RemoteException ignored) {
}
}
Log.i(this, "Call added to components: %s", componentsUpdated);
}
}
}
@Override
public void onCallRemoved(Call call) {
Log.i(this, "onCallRemoved: %s", call);
// Instead of checking if there are no active calls, we should check if there any calls with
// the same associated user returned from getUserFromCall. For instance, it's possible to
// have calls coexist on the personal profile and work profile, in which case, we would only
// remove the ICS connection for the user associated with the call to be disconnected.
UserHandle userFromCall = getUserFromCall(call);
Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
.filter((c) -> getUserFromCall(c).equals(userFromCall));
boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
? callsAssociatedWithUserFromCall.count() == 0
: mCallsManager.getCalls().isEmpty();
if (isCallCountZero) {
/** Let's add a 2 second delay before we send unbind to the services to hopefully
* give them enough time to process all the pending messages.
*/
mHandler.postDelayed(new Runnable("ICC.oCR", mLock) {
@Override
public void loggedRun() {
// Check again to make sure there are no active calls for the associated user.
Stream<Call> callsAssociatedWithUserFromCall = mCallsManager.getCalls().stream()
.filter((c) -> getUserFromCall(c).equals(userFromCall));
boolean isCallCountZero = mFeatureFlags.associatedUserRefactorForWorkProfile()
? callsAssociatedWithUserFromCall.count() == 0
: mCallsManager.getCalls().isEmpty();
if (isCallCountZero) {
unbindFromServices(userFromCall);
mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
}
}
}.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()));
}
call.removeListener(mCallListener);
mCallIdMapper.removeCall(call);
if (mCallIdMapper.getCalls().isEmpty()) {
mActiveCarrierPrivilegedApps.clear();
mAppOpsManager.stopWatchingActive(this);
}
maybeTrackMicrophoneUse(isMuted());
onSetCamera(call, null);
}
@Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {
Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
List<ComponentName> componentsUpdated = new ArrayList<>();
UserHandle userFromCall = getUserFromCall(call);
if (!isExternalCall && mInCallServices.containsKey(userFromCall)) {
// The call was external but it is no longer external. We must now add it to any
// InCallServices which do not support external calls.
for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
if (info.isExternalCallsSupported()) {
// For InCallServices which support external calls, the call will have already
// been added to the connection service, so we do not need to add it again.
continue;
}
if (call.isSelfManaged() && !call.visibleToInCallService()
&& !info.isSelfManagedCallsSupported()) {
continue;
}
componentsUpdated.add(info.getComponentName());
IInCallService inCallService = entry.getValue();
// Only send the RTT call if it's a UI in-call service
boolean includeRttCall = info.equals(mInCallServiceConnections.
get(userFromCall).getInfo());
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(), includeRttCall,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
|| info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
try {
inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
updateCallTracking(call, info, true /* isAdd */);
} catch (RemoteException ignored) {
}
}
Log.i(this, "Previously external call added to components: %s", componentsUpdated);
} else {
// The call was regular but it is now external. We must now remove it from any
// InCallServices which do not support external calls.
// Remove the call by sending a call update indicating the call was disconnected.
Log.i(this, "Removing external call %s", call);
if (mInCallServices.containsKey(userFromCall)) {
for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
if (info.isExternalCallsSupported()) {
// For InCallServices which support external calls, we do not need to remove
// the call.
continue;
}
componentsUpdated.add(info.getComponentName());
IInCallService inCallService = entry.getValue();
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
call,
false /* includeVideoProvider */,
mCallsManager.getPhoneAccountRegistrar(),
false /* supportsExternalCalls */,
android.telecom.Call.STATE_DISCONNECTED /* overrideState */,
false /* includeRttCall */,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
|| info.getType() == IN_CALL_SERVICE_TYPE_NON_UI
);
try {
inCallService.updateCall(
sanitizeParcelableCallForService(info, parcelableCall));
} catch (RemoteException ignored) {
}
}
Log.i(this, "External call removed from components: %s", componentsUpdated);
}
}
maybeTrackMicrophoneUse(isMuted());
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
maybeTrackMicrophoneUse(isMuted());
updateCall(call);
}
@Override
public void onConnectionServiceChanged(
Call call,
ConnectionServiceWrapper oldService,
ConnectionServiceWrapper newService) {
updateCall(call);
}
@Override
public void onCallAudioStateChanged(CallAudioState oldCallAudioState,
CallAudioState newCallAudioState) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldCallAudioState,
newCallAudioState);
maybeTrackMicrophoneUse(newCallAudioState.isMuted());
mInCallServices.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCallAudioStateChanged(newCallAudioState);
} catch (RemoteException ignored) {
}
}
});
}
}
@Override
public void onCallEndpointChanged(CallEndpoint callEndpoint) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onCallEndpointChanged");
mInCallServices.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCallEndpointChanged(callEndpoint);
} catch (RemoteException ignored) {
Log.d(this, "Remote exception calling onCallEndpointChanged");
}
}
});
}
}
@Override
public void onAvailableCallEndpointsChanged(Set<CallEndpoint> availableCallEndpoints) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onAvailableCallEndpointsChanged");
List<CallEndpoint> availableEndpoints = new ArrayList<>(availableCallEndpoints);
mInCallServices.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onAvailableCallEndpointsChanged(availableEndpoints);
} catch (RemoteException ignored) {
Log.d(this, "Remote exception calling onAvailableCallEndpointsChanged");
}
}
});
}
}
@Override
public void onMuteStateChanged(boolean isMuted) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onMuteStateChanged");
mInCallServices.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onMuteStateChanged(isMuted);
} catch (RemoteException ignored) {
Log.d(this, "Remote exception calling onMuteStateChanged");
}
}
});
}
}
@Override
public void onCanAddCallChanged(boolean canAddCall) {
if (!mInCallServices.isEmpty()) {
Log.i(this, "onCanAddCallChanged : %b", canAddCall);
mInCallServices.values().forEach(inCallServices -> {
for (IInCallService inCallService : inCallServices.values()) {
try {
inCallService.onCanAddCallChanged(canAddCall);
} catch (RemoteException ignored) {
}
}
});
}
}
void onPostDialWait(Call call, String remaining) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
} catch (RemoteException ignored) {
}
}
}
}
@Override
public void onIsConferencedChanged(Call call) {
Log.d(this, "onIsConferencedChanged %s", call);
updateCall(call);
}
@Override
public void onConnectionTimeChanged(Call call) {
Log.d(this, "onConnectionTimeChanged %s", call);
updateCall(call);
}
@Override
public void onIsVoipAudioModeChanged(Call call) {
Log.d(this, "onIsVoipAudioModeChanged %s", call);
updateCall(call);
maybeTrackMicrophoneUse(isMuted());
}
@Override
public void onConferenceStateChanged(Call call, boolean isConference) {
Log.d(this, "onConferenceStateChanged %s ,isConf=%b", call, isConference);
updateCall(call);
}
@Override
public void onCdmaConferenceSwap(Call call) {
Log.d(this, "onCdmaConferenceSwap %s", call);
updateCall(call);
}
/**
* Track changes to camera usage for a call.
*
* @param call The call.
* @param cameraId The id of the camera to use, or {@code null} if camera is off.
*/
@Override
public void onSetCamera(Call call, String cameraId) {
if (call == null) {
return;
}
Log.i(this, "onSetCamera callId=%s, cameraId=%s", call.getId(), cameraId);
if (cameraId != null) {
boolean shouldStart = mCallsUsingCamera.isEmpty();
if (!mCallsUsingCamera.contains(call.getId())) {
mCallsUsingCamera.add(call.getId());
}
if (shouldStart) {
// Note, not checking return value, as this op call is merely for tracing use
mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
mContext.getOpPackageName(), false, null, null);
mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.CAMERA);
}
} else {
boolean hadCall = !mCallsUsingCamera.isEmpty();
mCallsUsingCamera.remove(call.getId());
if (hadCall && mCallsUsingCamera.isEmpty()) {
mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_CAMERA, myUid(),
mContext.getOpPackageName(), null);
}
}
}
@VisibleForTesting
public void bringToForeground(boolean showDialpad, UserHandle callingUser) {
KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class);
boolean isLockscreenRestricted = keyguardManager != null
&& keyguardManager.isKeyguardLocked();
UserHandle currentUser = mCallsManager.getCurrentUserHandle();
// Handle cases when calls are placed from the keyguard UI screen, which operates under
// the admin user. This needs to account for emergency calls placed from secondary/guest
// users as well as the work profile. Once the screen is locked, the user should be able to
// return to the call (from the keyguard UI).
if (mFeatureFlags.eccKeyguard() && mCallsManager.isInEmergencyCall()
&& isLockscreenRestricted && !mInCallServices.containsKey(callingUser)) {
// If screen is locked and the current user is the system, query calls for the work
// profile user, if available. Otherwise, the user is in the secondary/guest profile,
// so we can default to the system user.
if (currentUser.isSystem()) {
UserManager um = mContext.getSystemService(UserManager.class);
UserHandle workProfileUser = findChildManagedProfileUser(currentUser, um);
boolean hasWorkCalls = mCallsManager.getCalls().stream()
.filter((c) -> getUserFromCall(c).equals(workProfileUser)).count() > 0;
callingUser = hasWorkCalls ? workProfileUser : currentUser;
} else {
callingUser = currentUser;
}
}
if (mInCallServices.containsKey(callingUser)) {
for (IInCallService inCallService : mInCallServices.get(callingUser).values()) {
try {
inCallService.bringToForeground(showDialpad);
} catch (RemoteException ignored) {
}
}
} else {
Log.w(this, "Asking to bring unbound in-call UI to foreground.");
}
}
@VisibleForTesting
public Map<UserHandle, Map<InCallServiceInfo, IInCallService>> getInCallServices() {
return mInCallServices;
}
@VisibleForTesting
public Map<UserHandle, CarSwappingInCallServiceConnection> getInCallServiceConnections() {
return mInCallServiceConnections;
}
void silenceRinger(Set<UserHandle> userHandles) {
userHandles.forEach(userHandle -> {
if (mInCallServices.containsKey(userHandle)) {
for (IInCallService inCallService : mInCallServices.get(userHandle).values()) {
try {
inCallService.silenceRinger();
} catch (RemoteException ignored) {
}
}
}
});
}
private void notifyConnectionEvent(Call call, String event, Bundle extras) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
Log.i(this, "notifyConnectionEvent {Call: %s, Event: %s, Extras:[%s]}",
(call != null ? call.toString() : "null"),
(event != null ? event : "null"),
(extras != null ? extras.toString() : "null"));
inCallService.onConnectionEvent(mCallIdMapper.getCallId(call), event, extras);
} catch (RemoteException ignored) {
}
}
}
}
private void notifyRttInitiationFailure(Call call, int reason) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
mInCallServices.get(userFromCall).entrySet().stream()
.filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
get(userFromCall).getInfo()))
.forEach((entry) -> {
try {
Log.i(this, "notifyRttFailure, call %s, incall %s",
call, entry.getKey());
entry.getValue().onRttInitiationFailure(mCallIdMapper.getCallId(call),
reason);
} catch (RemoteException ignored) {
}
});
}
}
private void notifyRemoteRttRequest(Call call, int requestId) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
mInCallServices.get(userFromCall).entrySet().stream()
.filter((entry) -> entry.getKey().equals(mInCallServiceConnections.
get(userFromCall).getInfo()))
.forEach((entry) -> {
try {
Log.i(this, "notifyRemoteRttRequest, call %s, incall %s",
call, entry.getKey());
entry.getValue().onRttUpgradeRequest(
mCallIdMapper.getCallId(call), requestId);
} catch (RemoteException ignored) {
}
});
}
}
private void notifyHandoverFailed(Call call, int error) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.onHandoverFailed(mCallIdMapper.getCallId(call), error);
} catch (RemoteException ignored) {
}
}
}
}
private void notifyHandoverComplete(Call call) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
for (IInCallService inCallService : mInCallServices.get(userFromCall).values()) {
try {
inCallService.onHandoverComplete(mCallIdMapper.getCallId(call));
} catch (RemoteException ignored) {
}
}
}
}
/**
* Unbinds an existing bound connection to the in-call app.
*/
public void unbindFromServices(UserHandle userHandle) {
Log.i(this, "Unbinding from services for user %s", userHandle);
try {
mContext.unregisterReceiver(mPackageChangedReceiver);
} catch (IllegalArgumentException e) {
// Ignore this -- we may or may not have registered it, but when we bind, we want to
// unregister no matter what.
}
if (mInCallServiceConnections.containsKey(userHandle)) {
mInCallServiceConnections.get(userHandle).disconnect();
mInCallServiceConnections.remove(userHandle);
}
if (mNonUIInCallServiceConnections.containsKey(userHandle)) {
mNonUIInCallServiceConnections.get(userHandle).disconnect();
mNonUIInCallServiceConnections.remove(userHandle);
}
mInCallServices.remove(userHandle);
}
/**
* Binds to all the UI-providing InCallService as well as system-implemented non-UI
* InCallServices. Method-invoker must check {@link #isBoundAndConnectedToServices()}
* before invoking.
*
* @param call The newly added call that triggered the binding to the in-call services.
*/
@VisibleForTesting
public void bindToServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserHandle parentUser = null;
UserManager um = mContext.getSystemService(UserManager.class);
if (um.isManagedProfile(userFromCall.getIdentifier())) {
parentUser = um.getProfileParent(userFromCall);
Log.i(this, "child:%s parent:%s", userFromCall, parentUser);
}
if (!mInCallServiceConnections.containsKey(userFromCall)) {
InCallServiceConnection dialerInCall = null;
InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent(userFromCall);
Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
if (defaultDialerComponentInfo != null &&
!defaultDialerComponentInfo.getComponentName().equals(
mDefaultDialerCache.getSystemDialerComponent())) {
dialerInCall = new InCallServiceBindingConnection(defaultDialerComponentInfo);
}
Log.i(this, "defaultDialer: " + dialerInCall);
InCallServiceInfo systemInCallInfo = getInCallServiceComponent(userFromCall,
mDefaultDialerCache.getSystemDialerComponent(), IN_CALL_SERVICE_TYPE_SYSTEM_UI);
EmergencyInCallServiceConnection systemInCall =
new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
systemInCall.setHasEmergency(mCallsManager.isInEmergencyCall());
InCallServiceConnection carModeInCall = null;
InCallServiceInfo carModeComponentInfo = getCurrentCarModeComponent(userFromCall);
InCallServiceInfo carModeComponentInfoForParentUser = null;
if(parentUser != null) {
//query using parent user too
carModeComponentInfoForParentUser = getCurrentCarModeComponent(
parentUser);
}
if (carModeComponentInfo != null &&
!carModeComponentInfo.getComponentName().equals(
mDefaultDialerCache.getSystemDialerComponent())) {
carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
} else if (carModeComponentInfo == null &&
carModeComponentInfoForParentUser != null &&
!carModeComponentInfoForParentUser.getComponentName().equals(
mDefaultDialerCache.getSystemDialerComponent())) {
carModeInCall = new InCallServiceBindingConnection(
carModeComponentInfoForParentUser, parentUser);
Log.i(this, "Using car mode component queried using parent handle");
}
mInCallServiceConnections.put(userFromCall,
new CarSwappingInCallServiceConnection(systemInCall, carModeInCall));
}
CarSwappingInCallServiceConnection inCallServiceConnection =
mInCallServiceConnections.get(userFromCall);
inCallServiceConnection.chooseInitialInCallService(shouldUseCarModeUI());
// Actually try binding to the UI InCallService.
if (inCallServiceConnection.connect(call) ==
InCallServiceConnection.CONNECTION_SUCCEEDED || call.isSelfManaged()) {
// Only connect to the non-ui InCallServices if we actually connected to the main UI
// one, or if the call is self-managed (in which case we'd still want to keep Wear, BT,
// etc. informed.
connectToNonUiInCallServices(call);
mBindingFuture = new CompletableFuture<Boolean>().completeOnTimeout(false,
mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
mContext.getContentResolver()),
TimeUnit.MILLISECONDS);
} else {
Log.i(this, "bindToServices: current UI doesn't support call; not binding.");
}
IntentFilter packageChangedFilter = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED);
packageChangedFilter.addDataScheme("package");
mContext.registerReceiverAsUser(mPackageChangedReceiver, UserHandle.ALL,
packageChangedFilter, null, null);
}
private void updateNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
UserHandle parentUser = null;
UserManager um = mContext.getSystemService(UserManager.class);
if(um.isManagedProfile(userFromCall.getIdentifier()))
{
parentUser = um.getProfileParent(userFromCall);
}
List<InCallServiceInfo> nonUIInCallComponents =
getInCallServiceComponents(userFromCall, IN_CALL_SERVICE_TYPE_NON_UI);
List<InCallServiceInfo> nonUIInCallComponentsForParent = new ArrayList<>();
if(parentUser != null)
{
//also get Non-UI services using parent handle.
nonUIInCallComponentsForParent =
getInCallServiceComponents(parentUser, IN_CALL_SERVICE_TYPE_NON_UI);
}
List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
}
//add nonUI InCall services queried using parent user (if any)
for (InCallServiceInfo serviceInfo : nonUIInCallComponentsForParent) {
if (nonUIInCallComponents.contains(serviceInfo)) {
//skip dups
Log.i(this, "skipped duplicate component found using parent user: "
+ serviceInfo.getComponentName());
} else {
nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo, parentUser));
Log.i(this,
"added component queried using parent user: "
+ serviceInfo.getComponentName());
}
}
List<String> callCompanionApps = mCallsManager
.getRoleManagerAdapter().getCallCompanionApps();
if (callCompanionApps != null && !callCompanionApps.isEmpty()) {
for (String pkg : callCompanionApps) {
InCallServiceInfo info = getInCallServiceComponent(userFromCall, pkg,
IN_CALL_SERVICE_TYPE_COMPANION, true /* ignoreDisabled */);
if (info != null) {
nonUIInCalls.add(new InCallServiceBindingConnection(info));
}
}
}
mNonUIInCallServiceConnections.put(userFromCall, new NonUIInCallServiceConnectionCollection(
nonUIInCalls));
}
private void connectToNonUiInCallServices(Call call) {
UserHandle userFromCall = getUserFromCall(call);
if (!mNonUIInCallServiceConnections.containsKey(userFromCall)) {
updateNonUiInCallServices(call);
}
mNonUIInCallServiceConnections.get(userFromCall).connect(call);
}
private @Nullable InCallServiceInfo getDefaultDialerComponent(UserHandle userHandle) {
String defaultPhoneAppName = mDefaultDialerCache.getDefaultDialerApplication(
userHandle.getIdentifier());
String systemPhoneAppName = mDefaultDialerCache.getSystemDialerApplication();
Log.d(this, "getDefaultDialerComponent: defaultPhoneAppName=[%s]", defaultPhoneAppName);
Log.d(this, "getDefaultDialerComponent: systemPhoneAppName=[%s]", systemPhoneAppName);
// Get the defaultPhoneApp InCallService component...
InCallServiceInfo defaultPhoneAppComponent =
(systemPhoneAppName != null && systemPhoneAppName.equals(defaultPhoneAppName)) ?
/* The defaultPhoneApp is also the systemPhoneApp. Get systemPhoneApp info*/
getInCallServiceComponent(userHandle, defaultPhoneAppName,
IN_CALL_SERVICE_TYPE_SYSTEM_UI, true /* ignoreDisabled */)
/* The defaultPhoneApp is NOT the systemPhoneApp. Get defaultPhoneApp info*/
: getInCallServiceComponent(userHandle, defaultPhoneAppName,
IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI, true /* ignoreDisabled */);
Log.d(this, "getDefaultDialerComponent: defaultPhoneAppComponent=[%s]",
defaultPhoneAppComponent);
// defaultPhoneAppComponent is null in the case when the defaultPhoneApp does not implement
// the InCallService && is the package is different from the systemPhoneApp
return defaultPhoneAppComponent;
}
private InCallServiceInfo getCurrentCarModeComponent(UserHandle userHandle) {
return getInCallServiceComponent(userHandle,
mCarModeTracker.getCurrentCarModePackage(),
IN_CALL_SERVICE_TYPE_CAR_MODE_UI, true /* ignoreDisabled */);
}
private InCallServiceInfo getInCallServiceComponent(UserHandle userHandle,
ComponentName componentName, int type) {
List<InCallServiceInfo> list = getInCallServiceComponents(userHandle,
componentName, type);
if (list != null && !list.isEmpty()) {
return list.get(0);
} else {
// Last Resort: Try to bind to the ComponentName given directly.
Log.e(this, new Exception(), "Package Manager could not find ComponentName: "
+ componentName + ". Trying to bind anyway.");
return new InCallServiceInfo(componentName, false, false, type, false);
}
}
private InCallServiceInfo getInCallServiceComponent(UserHandle userHandle,
String packageName, int type, boolean ignoreDisabled) {
List<InCallServiceInfo> list = getInCallServiceComponents(userHandle,
packageName, type, ignoreDisabled);
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
private List<InCallServiceInfo> getInCallServiceComponents(
UserHandle userHandle, int type) {
return getInCallServiceComponents(userHandle, null, null, type);
}
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, int type, boolean ignoreDisabled) {
return getInCallServiceComponents(userHandle, packageName, null,
type, ignoreDisabled);
}
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
ComponentName componentName, int type) {
return getInCallServiceComponents(userHandle, null, componentName, type);
}
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, ComponentName componentName, int requestedType) {
return getInCallServiceComponents(userHandle, packageName,
componentName, requestedType, true /* ignoreDisabled */);
}
private boolean canInteractAcrossUsersOrProfiles(ServiceInfo serviceInfo,
PackageManager packageManager) {
String op = AppOpsManager.permissionToOp("android.permission.INTERACT_ACROSS_PROFILES");
String[] uidPackages = packageManager.getPackagesForUid(serviceInfo.applicationInfo.uid);
boolean hasInteractAcrossProfiles = Arrays.stream(uidPackages).anyMatch(
p -> ((packageManager.checkPermission(
Manifest.permission.INTERACT_ACROSS_PROFILES,
p) == PackageManager.PERMISSION_GRANTED)
));
boolean hasInteractAcrossUsers = Arrays.stream(uidPackages).anyMatch(
p -> ((packageManager.checkPermission(
Manifest.permission.INTERACT_ACROSS_USERS,
p) == PackageManager.PERMISSION_GRANTED)
));
boolean hasInteractAcrossProfilesAppOp = Arrays.stream(uidPackages).anyMatch(
p -> (AppOpsManager.MODE_ALLOWED == mAppOpsManager.checkOpNoThrow(
op, serviceInfo.applicationInfo.uid, p))
);
Log.i(this,
"packageName:%s INTERACT_ACROSS_USERS:%b INTERACT_ACROSS_PROFILES:%b "
+ "INTERACT_ACROSS_PROFILES_APPOP:%b",
uidPackages[0], hasInteractAcrossUsers, hasInteractAcrossProfiles,
hasInteractAcrossProfilesAppOp);
return (hasInteractAcrossUsers || hasInteractAcrossProfiles
|| hasInteractAcrossProfilesAppOp);
}
private List<InCallServiceInfo> getInCallServiceComponents(UserHandle userHandle,
String packageName, ComponentName componentName,
int requestedType, boolean ignoreDisabled) {
List<InCallServiceInfo> retval = new LinkedList<>();
Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
if (packageName != null) {
serviceIntent.setPackage(packageName);
}
if (componentName != null) {
serviceIntent.setComponent(componentName);
}
Log.i(this,
"getComponents, pkgname: " + packageName + " comp: " + componentName + " userid: "
+ userHandle.getIdentifier() + " requestedType: " + requestedType);
PackageManager packageManager = mContext.getPackageManager();
Context userContext = mContext.createContextAsUser(userHandle,
0 /* flags */);
PackageManager userPackageManager = userContext != null ?
userContext.getPackageManager() : packageManager;
for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
serviceIntent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_COMPONENTS,
userHandle.getIdentifier())) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
boolean isExternalCallsSupported = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, false);
boolean isSelfManageCallsSupported = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
TelecomManager.METADATA_INCLUDE_SELF_MANAGED_CALLS, false);
int currentType = getInCallServiceType(userHandle,
entry.serviceInfo, packageManager, packageName);
boolean hasInteractAcrossUserOrProfilePerm = canInteractAcrossUsersOrProfiles(
entry.serviceInfo, packageManager);
ComponentName foundComponentName =
new ComponentName(serviceInfo.packageName, serviceInfo.name);
if (currentType == IN_CALL_SERVICE_TYPE_NON_UI) {
mKnownNonUiInCallServices.add(foundComponentName);
}
boolean isEnabled = isServiceEnabled(foundComponentName,
serviceInfo, userPackageManager);
boolean isRequestedType;
if (requestedType == IN_CALL_SERVICE_TYPE_INVALID) {
isRequestedType = true;
} else {
isRequestedType = requestedType == currentType;
}
Log.i(this,
"found:%s isRequestedtype:%b isEnabled:%b ignoreDisabled:%b "
+ "hasCrossProfilePerm:%b",
foundComponentName, isRequestedType, isEnabled, ignoreDisabled,
hasInteractAcrossUserOrProfilePerm);
if ((!ignoreDisabled || isEnabled) && isRequestedType) {
retval.add(new InCallServiceInfo(foundComponentName, isExternalCallsSupported,
isSelfManageCallsSupported, requestedType,
hasInteractAcrossUserOrProfilePerm));
}
}
}
return retval;
}
private boolean isServiceEnabled(ComponentName componentName,
ServiceInfo serviceInfo, PackageManager packageManager) {
if (packageManager == null) {
return serviceInfo.isEnabled();
}
int componentEnabledState = packageManager.getComponentEnabledSetting(componentName);
if (componentEnabledState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
return true;
}
if (componentEnabledState == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
return serviceInfo.isEnabled();
}
return false;
}
private boolean shouldUseCarModeUI() {
return mCarModeTracker.isInCarMode();
}
/**
* Returns the type of InCallService described by the specified serviceInfo.
*/
private int getInCallServiceType(UserHandle userHandle, ServiceInfo serviceInfo,
PackageManager packageManager, String packageName) {
// Verify that the InCallService requires the BIND_INCALL_SERVICE permission which
// enforces that only Telecom can bind to it.
boolean hasServiceBindPermission = serviceInfo.permission != null &&
serviceInfo.permission.equals(
Manifest.permission.BIND_INCALL_SERVICE);
if (!hasServiceBindPermission) {
Log.w(this, "InCallService does not require BIND_INCALL_SERVICE permission: " +
serviceInfo.packageName);
return IN_CALL_SERVICE_TYPE_INVALID;
}
if (mDefaultDialerCache.getSystemDialerApplication().equals(serviceInfo.packageName) &&
mDefaultDialerCache.getSystemDialerComponent().getClassName()
.equals(serviceInfo.name)) {
return IN_CALL_SERVICE_TYPE_SYSTEM_UI;
}
// Check to see if the service holds permissions or metadata for third party apps.
boolean isUIService = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(TelecomManager.METADATA_IN_CALL_SERVICE_UI);
// Check to see if the service is a car-mode UI type by checking that it has the
// CONTROL_INCALL_EXPERIENCE (to verify it is a system app) and that it has the
// car-mode UI metadata.
// We check the permission grant on all of the packages contained in the InCallService's
// same UID to see if any of them have been granted the permission. This accomodates the
// CTS tests, which have some shared UID stuff going on in order to work. It also still
// obeys the permission model since a single APK typically normally only has a single UID.
String[] uidPackages = packageManager.getPackagesForUid(serviceInfo.applicationInfo.uid);
boolean hasControlInCallPermission = Arrays.stream(uidPackages).anyMatch(
p -> packageManager.checkPermission(
Manifest.permission.CONTROL_INCALL_EXPERIENCE,
p) == PackageManager.PERMISSION_GRANTED);
boolean hasAppOpsPermittedManageOngoingCalls = false;
if (isAppOpsPermittedManageOngoingCalls(serviceInfo.applicationInfo.uid,
serviceInfo.packageName)) {
hasAppOpsPermittedManageOngoingCalls = true;
}
boolean isCarModeUIService = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
TelecomManager.METADATA_IN_CALL_SERVICE_CAR_MODE_UI, false);
if (isCarModeUIService && hasControlInCallPermission) {
return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
}
// Check to see that it is the default dialer package
boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
mDefaultDialerCache.getDefaultDialerApplication(
userHandle.getIdentifier()));
if (isDefaultDialerPackage && isUIService) {
return IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
}
// Also allow any in-call service that has the control-experience permission (to ensure
// that it is a system app) and doesn't claim to show any UI.
if (!isUIService && !isCarModeUIService && (hasControlInCallPermission ||
hasAppOpsPermittedManageOngoingCalls)) {
return IN_CALL_SERVICE_TYPE_NON_UI;
}
// Anything else that remains, we will not bind to.
Log.i(this, "Skipping binding to %s:%s, control: %b, car-mode: %b, ui: %b",
serviceInfo.packageName, serviceInfo.name, hasControlInCallPermission,
isCarModeUIService, isUIService);
return IN_CALL_SERVICE_TYPE_INVALID;
}
private void adjustServiceBindingsForEmergency(UserHandle userHandle) {
// The connected UI is not the system UI, so lets check if we should switch them
// if there exists an emergency number.
if (mCallsManager.isInEmergencyCall()) {
mInCallServiceConnections.get(userHandle).setHasEmergency(true);
}
}
/**
* Persists the {@link IInCallService} instance and starts the communication between
* this class and in-call app by sending the first update to in-call app. This method is
* called after a successful binding connection is established.
*
* @param info Info about the service, including its {@link ComponentName}.
* @param service The {@link IInCallService} implementation.
* @return True if we successfully connected.
*/
private boolean onConnected(InCallServiceInfo info, IBinder service, UserHandle userHandle) {
Log.i(this, "onConnected to %s", info.getComponentName());
if (info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
|| info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
|| info.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI) {
trackCallingUserInterfaceStarted(info);
}
IInCallService inCallService = IInCallService.Stub.asInterface(service);
mInCallServices.putIfAbsent(userHandle,
new ArrayMap<InCallController.InCallServiceInfo, IInCallService>());
mInCallServices.get(userHandle).put(info, inCallService);
try {
inCallService.setInCallAdapter(
new InCallAdapter(
mCallsManager,
mCallIdMapper,
mLock,
info.getComponentName().getPackageName()));
} catch (RemoteException e) {
Log.e(this, e, "Failed to set the in-call adapter.");
mAnomalyReporter.reportAnomaly(SET_IN_CALL_ADAPTER_ERROR_UUID,
SET_IN_CALL_ADAPTER_ERROR_MSG);
Trace.endSection();
return false;
}
// Upon successful connection, send the state of the world to the service.
List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls().stream().filter(
call -> getUserFromCall(call).equals(userHandle))
.collect(Collectors.toUnmodifiableList()));
Log.i(this, "Adding %s calls to InCallService after onConnected: %s, including external " +
"calls", calls.size(), info.getComponentName());
int numCallsSent = 0;
for (Call call : calls) {
numCallsSent += sendCallToService(call, info, inCallService);
}
try {
inCallService.onCallAudioStateChanged(mCallsManager.getAudioState());
inCallService.onCanAddCallChanged(mCallsManager.canAddCall());
} catch (RemoteException ignored) {
}
// Don't complete the binding future for non-ui incalls
if (info.getType() != IN_CALL_SERVICE_TYPE_NON_UI && !mBindingFuture.isDone()) {
mBindingFuture.complete(true);
}
Log.i(this, "%s calls sent to InCallService.", numCallsSent);
return true;
}
private int sendCallToService(Call call, InCallServiceInfo info,
IInCallService inCallService) {
try {
if ((call.isSelfManaged() && (!info.isSelfManagedCallsSupported()
|| !call.visibleToInCallService())) ||
(call.isExternalCall() && !info.isExternalCallsSupported())) {
return 0;
}
UserHandle userFromCall = getUserFromCall(call);
// Only send the RTT call if it's a UI in-call service
boolean includeRttCall = false;
if (mInCallServiceConnections.containsKey(userFromCall)) {
includeRttCall = info.equals(mInCallServiceConnections.get(userFromCall).getInfo());
}
// Track the call if we don't already know about it.
addCall(call);
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
call,
true /* includeVideoProvider */,
mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(),
includeRttCall,
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
inCallService.addCall(sanitizeParcelableCallForService(info, parcelableCall));
updateCallTracking(call, info, true /* isAdd */);
return 1;
} catch (RemoteException ignored) {
}
return 0;
}
/**
* Cleans up an instance of in-call app after the service has been unbound.
*
* @param disconnectedInfo The {@link InCallServiceInfo} of the service which disconnected.
*/
private void onDisconnected(InCallServiceInfo disconnectedInfo, UserHandle userHandle) {
Log.i(this, "onDisconnected from %s", disconnectedInfo.getComponentName());
if (disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
|| disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI
|| disconnectedInfo.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI) {
trackCallingUserInterfaceStopped(disconnectedInfo);
}
if (mInCallServices.containsKey(userHandle)) {
mInCallServices.get(userHandle).remove(disconnectedInfo);
}
}
/**
* Informs all {@link InCallService} instances of the updated call information.
*
* @param call The {@link Call}.
*/
private void updateCall(Call call) {
updateCall(call, false /* videoProviderChanged */, false, null);
}
/**
* Informs all {@link InCallService} instances of the updated call information.
*
* @param call The {@link Call}.
* @param videoProviderChanged {@code true} if the video provider changed, {@code false}
* otherwise.
* @param rttInfoChanged {@code true} if any information about the RTT session changed,
* {@code false} otherwise.
* @param exceptPackageName When specified, this package name will not get a call update.
* Used ONLY from {@link Call#putConnectionServiceExtras(int, Bundle, String)} to
* ensure we can propagate extras changes between InCallServices but
* not inform the requestor of their own change.
*/
private void updateCall(Call call, boolean videoProviderChanged, boolean rttInfoChanged,
String exceptPackageName) {
UserHandle userFromCall = getUserFromCall(call);
if (mInCallServices.containsKey(userFromCall)) {
Log.i(this, "Sending updateCall %s", call);
List<ComponentName> componentsUpdated = new ArrayList<>();
for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.
get(userFromCall).entrySet()) {
InCallServiceInfo info = entry.getKey();
ComponentName componentName = info.getComponentName();
// If specified, skip ICS if it matches the package name. Used for cases where on
// ICS makes an update to extras and we want to skip updating the same ICS with the
// change that it implemented.
if (exceptPackageName != null
&& componentName.getPackageName().equals(exceptPackageName)) {
continue;
}
if (call.isExternalCall() && !info.isExternalCallsSupported()) {
continue;
}
if (call.isSelfManaged() && (!call.visibleToInCallService()
|| !info.isSelfManagedCallsSupported())) {
continue;
}
ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(
call,
videoProviderChanged /* includeVideoProvider */,
mCallsManager.getPhoneAccountRegistrar(),
info.isExternalCallsSupported(),
rttInfoChanged && info.equals(
mInCallServiceConnections.get(userFromCall).getInfo()),
info.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI ||
info.getType() == IN_CALL_SERVICE_TYPE_NON_UI);
IInCallService inCallService = entry.getValue();
componentsUpdated.add(componentName);
try {
inCallService.updateCall(
sanitizeParcelableCallForService(info, parcelableCall));
} catch (RemoteException exception) {
Log.w(this, "Call status update did not send to: "
+ componentName +" successfully with error " + exception);
}
}
Log.i(this, "Components updated: %s", componentsUpdated);
} else {
Log.i(this,
"Unable to update call. InCallService not found for user: %s", userFromCall);
}
}
/**
* Adds the call to the list of calls tracked by the {@link InCallController}.
* @param call The call to add.
*/
@VisibleForTesting
public void addCall(Call call) {
if (mCallIdMapper.getCalls().size() == 0) {
mAppOpsManager.startWatchingActive(new String[] { OPSTR_RECORD_AUDIO },
java.lang.Runnable::run, this);
updateAllCarrierPrivileged();
updateAllCarrierPrivilegedUsingMic();
}
if (mCallIdMapper.getCallId(call) == null) {
mCallIdMapper.addCall(call);
call.addListener(mCallListener);
}
maybeTrackMicrophoneUse(isMuted());
}
/**
* @return true if we are bound to the UI InCallService and it is connected.
*/
private boolean isBoundAndConnectedToServices(UserHandle userHandle) {
if (!mInCallServiceConnections.containsKey(userHandle)) {
return false;
}
return mInCallServiceConnections.get(userHandle).isConnected();
}
/**
* @return A future that is pending whenever we are in the middle of binding to an
* incall service.
*/
public CompletableFuture<Boolean> getBindingFuture() {
return mBindingFuture;
}
/**
* Dumps the state of the {@link InCallController}.
*
* @param pw The {@code IndentingPrintWriter} to write the state to.
*/
public void dump(IndentingPrintWriter pw) {
pw.println("mInCallServices (InCalls registered):");
pw.increaseIndent();
mInCallServices.values().forEach(inCallServices -> {
for (InCallServiceInfo info : inCallServices.keySet()) {
pw.println(info);
}
});
pw.decreaseIndent();
pw.println("ServiceConnections (InCalls bound):");
pw.increaseIndent();
for (InCallServiceConnection inCallServiceConnection : mInCallServiceConnections.values()) {
inCallServiceConnection.dump(pw);
}
pw.decreaseIndent();
mCarModeTracker.dump(pw);
}
/**
* @return The package name of the UI which is currently bound, or null if none.
*/
private ComponentName getConnectedUi(UserHandle userHandle) {
if (mInCallServices.containsKey(userHandle)) {
InCallServiceInfo connectedUi = mInCallServices.get(
userHandle).keySet().stream().filter(
i -> i.getType() == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI
|| i.getType() == IN_CALL_SERVICE_TYPE_SYSTEM_UI)
.findAny()
.orElse(null);
if (connectedUi != null) {
return connectedUi.mComponentName;
}
}
return null;
}
public boolean doesConnectedDialerSupportRinging(UserHandle userHandle) {
String ringingPackage = null;
ComponentName connectedPackage = getConnectedUi(userHandle);
if (connectedPackage != null) {
ringingPackage = connectedPackage.getPackageName().trim();
Log.d(this, "doesConnectedDialerSupportRinging: alreadyConnectedPackage=%s",
ringingPackage);
}
if (TextUtils.isEmpty(ringingPackage)) {
// The current in-call UI returned nothing, so lets use the default dialer.
ringingPackage = mDefaultDialerCache.getRoleManagerAdapter().getDefaultDialerApp(
userHandle.getIdentifier());
if (ringingPackage != null) {
Log.d(this, "doesConnectedDialerSupportRinging: notCurentlyConnectedPackage=%s",
ringingPackage);
}
}
if (TextUtils.isEmpty(ringingPackage)) {
Log.w(this, "doesConnectedDialerSupportRinging: no default dialer found; oh no!");
return false;
}
Intent intent = new Intent(InCallService.SERVICE_INTERFACE)
.setPackage(ringingPackage);
List<ResolveInfo> entries = mContext.getPackageManager().queryIntentServicesAsUser(
intent, PackageManager.GET_META_DATA,
userHandle.getIdentifier());
if (entries.isEmpty()) {
Log.w(this, "doesConnectedDialerSupportRinging: couldn't find dialer's package info"
+ " <sad trombone>");
return false;
}
ResolveInfo info = entries.get(0);
if (info.serviceInfo == null || info.serviceInfo.metaData == null) {
Log.w(this, "doesConnectedDialerSupportRinging: couldn't find dialer's metadata"
+ " <even sadder trombone>");
return false;
}
return info.serviceInfo.metaData
.getBoolean(TelecomManager.METADATA_IN_CALL_SERVICE_RINGING, false);
}
private List<Call> orderCallsWithChildrenFirst(Collection<Call> calls) {
LinkedList<Call> parentCalls = new LinkedList<>();
LinkedList<Call> childCalls = new LinkedList<>();
for (Call call : calls) {
if (call.getChildCalls().size() > 0) {
parentCalls.add(call);
} else {
childCalls.add(call);
}
}
childCalls.addAll(parentCalls);
return childCalls;
}
@VisibleForTesting
public ParcelableCall sanitizeParcelableCallForService(
InCallServiceInfo info, ParcelableCall parcelableCall) {
ParcelableCall.ParcelableCallBuilder builder =
ParcelableCall.ParcelableCallBuilder.fromParcelableCall(parcelableCall);
PackageManager pm = mContext.getPackageManager();
// Check for contacts permission.
if (pm.checkPermission(Manifest.permission.READ_CONTACTS,
info.getComponentName().getPackageName()) != PackageManager.PERMISSION_GRANTED) {
// contacts permission is not present...
// removing the contactsDisplayName
builder.setContactDisplayName(null);
builder.setContactPhotoUri(null);
// removing the Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB extra
if (parcelableCall.getExtras() != null) {
Bundle callBundle = parcelableCall.getExtras();
if (callBundle.containsKey(
android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB)) {
Bundle newBundle = callBundle.deepCopy();
newBundle.remove(android.telecom.Call.EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB);
builder.setExtras(newBundle);
}
}
}
// TODO: move all the other service-specific sanitizations in here
return builder.createParcelableCall();
}
@VisibleForTesting
public Handler getHandler() {
return mHandler;
}
/**
* Determines if the specified package is a valid car mode {@link InCallService}.
* @param packageName The package name to check.
* @return {@code true} if the package has a valid car mode {@link InCallService} defined,
* {@code false} otherwise.
*/
private boolean isCarModeInCallService(@NonNull String packageName) {
// Disabled InCallService should also be considered as a valid InCallService here so that
// it can be added to the CarModeTracker, in case it will be enabled in future.
InCallServiceInfo info =
getInCallServiceComponent(mCallsManager.getCurrentUserHandle(),
packageName, IN_CALL_SERVICE_TYPE_CAR_MODE_UI, false /* ignoreDisabled */);
return info != null && info.getType() == IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
}
public void handleCarModeChange(int priority, String packageName, boolean isCarMode) {
Log.i(this, "handleCarModeChange: packageName=%s, priority=%d, isCarMode=%b",
packageName, priority, isCarMode);
if (packageName == null) {
Log.i(this, "handleCarModeChange: Got null packageName, ignoring");
return;
}
// Don't ignore the signal if we are disabling car mode; package may be uninstalled.
if (isCarMode && !isCarModeInCallService(packageName)) {
Log.i(this, "handleCarModeChange: not a valid InCallService; packageName=%s",
packageName);
return;
}
if (isCarMode) {
mCarModeTracker.handleEnterCarMode(priority, packageName);
} else {
mCarModeTracker.handleExitCarMode(priority, packageName);
}
updateCarModeForConnections();
}
public void handleSetAutomotiveProjection(@NonNull String packageName) {
Log.i(this, "handleSetAutomotiveProjection: packageName=%s", packageName);
if (!isCarModeInCallService(packageName)) {
Log.i(this, "handleSetAutomotiveProjection: not a valid InCallService: packageName=%s",
packageName);
return;
}
mCarModeTracker.handleSetAutomotiveProjection(packageName);
updateCarModeForConnections();
}
public void handleReleaseAutomotiveProjection() {
Log.i(this, "handleReleaseAutomotiveProjection");
mCarModeTracker.handleReleaseAutomotiveProjection();
updateCarModeForConnections();
}
public void updateCarModeForConnections() {
Log.i(this, "updateCarModeForConnections: car mode apps: %s",
mCarModeTracker.getCarModeApps().stream().collect(Collectors.joining(", ")));
UserManager um = mContext.getSystemService(UserManager.class);
UserHandle currentUser = mCallsManager.getCurrentUserHandle();
UserHandle childUser = findChildManagedProfileUser(currentUser, um);
CarSwappingInCallServiceConnection inCallServiceConnectionForCurrentUser = null;
CarSwappingInCallServiceConnection inCallServiceConnectionForChildUser = null;
Log.i(this, "update carmode current:%s parent:%s", currentUser, childUser);
if (mInCallServiceConnections.containsKey(currentUser)) {
inCallServiceConnectionForCurrentUser = mInCallServiceConnections.
get(currentUser);
}
if (childUser != null && mInCallServiceConnections.containsKey(childUser)) {
inCallServiceConnectionForChildUser = mInCallServiceConnections.
get(childUser);
}
if (shouldUseCarModeUI()) {
Log.i(this, "updateCarModeForConnections: potentially update car mode app.");
//always pass current user to changeCarMode. That will ultimately be used for bindAsUser
if (inCallServiceConnectionForCurrentUser != null) {
inCallServiceConnectionForCurrentUser.changeCarModeApp(
mCarModeTracker.getCurrentCarModePackage(),
currentUser);
}
if (inCallServiceConnectionForChildUser != null) {
inCallServiceConnectionForChildUser.changeCarModeApp(
mCarModeTracker.getCurrentCarModePackage(),
currentUser);
}
} else {
if (inCallServiceConnectionForCurrentUser != null
&& inCallServiceConnectionForCurrentUser.isCarMode()) {
Log.i(this, "updateCarModeForConnections: car mode no longer "
+ "applicable for current user; disabling");
inCallServiceConnectionForCurrentUser.disableCarMode();
}
if (inCallServiceConnectionForChildUser != null
&& inCallServiceConnectionForChildUser.isCarMode()) {
Log.i(this, "updateCarModeForConnections: car mode no longer "
+ "applicable for child user; disabling");
inCallServiceConnectionForChildUser.disableCarMode();
}
}
}
/**
* Tracks start of microphone use on binding to the current calling UX.
* @param info
*/
private void trackCallingUserInterfaceStarted(InCallServiceInfo info) {
String packageName = info.getComponentName().getPackageName();
if (!Objects.equals(mCurrentUserInterfacePackageName, packageName)) {
Log.i(this, "trackCallingUserInterfaceStarted: %s is now calling UX.", packageName);
mCurrentUserInterfacePackageName = packageName;
}
maybeTrackMicrophoneUse(isMuted());
}
/**
* Tracks stop of microphone use on unbind from the current calling UX.
* @param info
*/
private void trackCallingUserInterfaceStopped(InCallServiceInfo info) {
maybeTrackMicrophoneUse(isMuted());
mCurrentUserInterfacePackageName = null;
String packageName = info.getComponentName().getPackageName();
Log.i(this, "trackCallingUserInterfaceStopped: %s is no longer calling UX", packageName);
}
private void maybeTrackMicrophoneUse(boolean isMuted) {
maybeTrackMicrophoneUse(isMuted, false);
}
/**
* As calls are added, removed and change between external and non-external status, track
* whether the current active calling UX is using the microphone. We assume if there is a
* managed call present and the mic is not muted that the microphone is in use.
*/
private void maybeTrackMicrophoneUse(boolean isMuted, boolean isScheduledDelay) {
if (mIsStartCallDelayScheduled && !isScheduledDelay) {
return;
}
mIsStartCallDelayScheduled = false;
boolean wasUsingMicrophone = mIsCallUsingMicrophone;
boolean wasTrackingCall = mIsTrackingManagedAliveCall;
mIsTrackingManagedAliveCall = isTrackingManagedAliveCall();
if (!wasTrackingCall && mIsTrackingManagedAliveCall) {
mIsStartCallDelayScheduled = true;
mHandler.postDelayed(new Runnable("ICC.mTMU", mLock) {
@Override
public void loggedRun() {
maybeTrackMicrophoneUse(isMuted(), true);
}
}.prepare(), mTimeoutsAdapter.getCallStartAppOpDebounceIntervalMillis());
return;
}
mIsCallUsingMicrophone = mIsTrackingManagedAliveCall && !isMuted
&& !isCarrierPrivilegedUsingMicDuringVoipCall();
if (wasUsingMicrophone != mIsCallUsingMicrophone) {
if (mIsCallUsingMicrophone) {
// Note, not checking return value, as this op call is merely for tracing use
mAppOpsManager.startOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
mContext.getOpPackageName(), false, null, null);
mSensorPrivacyManager.showSensorUseDialog(SensorPrivacyManager.Sensors.MICROPHONE);
} else {
mAppOpsManager.finishOp(AppOpsManager.OP_PHONE_CALL_MICROPHONE, myUid(),
mContext.getOpPackageName(), null);
}
}
}
/**
* @return {@code true} if InCallController is tracking a managed call (i.e. not self managed
* and not external) that is active.
*/
private boolean isTrackingManagedAliveCall() {
return mCallIdMapper.getCalls().stream().anyMatch(c -> !c.isExternalCall()
&& !c.isSelfManaged() && c.isAlive() && ArrayUtils.contains(LIVE_CALL_STATES,
c.getState()));
}
private boolean isCarrierPrivilegedUsingMicDuringVoipCall() {
return !mActiveCarrierPrivilegedApps.isEmpty() &&
mCallIdMapper.getCalls().stream().anyMatch(Call::getIsVoipAudioMode);
}
/**
* @return {@code true} if the audio is currently muted, {@code false} otherwise.
*/
private boolean isMuted() {
if (mCallsManager.getAudioState() == null) {
return false;
}
return mCallsManager.getAudioState().isMuted();
}
private boolean isAppOpsPermittedManageOngoingCalls(int uid, String callingPackage) {
return PermissionChecker.checkPermissionForDataDeliveryFromDataSource(mContext,
Manifest.permission.MANAGE_ONGOING_CALLS, PermissionChecker.PID_UNKNOWN,
new AttributionSource(mContext.getAttributionSource(),
new AttributionSource(uid, callingPackage,
/*attributionTag*/ null)), "Checking whether the app has"
+ " MANAGE_ONGOING_CALLS permission")
== PermissionChecker.PERMISSION_GRANTED;
}
private void sendCrashedInCallServiceNotification(String packageName, UserHandle userHandle) {
PackageManager packageManager = mContext.getPackageManager();
CharSequence appName;
String systemDialer = mDefaultDialerCache.getSystemDialerApplication();
if ((systemDialer != null) && systemDialer.equals(packageName)) {
return;
}
try {
appName = packageManager.getApplicationLabel(
packageManager.getApplicationInfo(packageName, 0));
if (TextUtils.isEmpty(appName)) {
appName = packageName;
}
} catch (PackageManager.NameNotFoundException e) {
appName = packageName;
}
NotificationManager notificationManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = new Notification.Builder(mContext,
NotificationChannelManager.CHANNEL_ID_IN_CALL_SERVICE_CRASH);
builder.setSmallIcon(R.drawable.ic_phone)
.setColor(mContext.getResources().getColor(R.color.theme_color))
.setContentTitle(
mContext.getString(
R.string.notification_incallservice_not_responding_title, appName))
.setStyle(new Notification.BigTextStyle()
.bigText(mContext.getText(
R.string.notification_incallservice_not_responding_body)));
notificationManager.notifyAsUser(NOTIFICATION_TAG, IN_CALL_SERVICE_NOTIFICATION_ID,
builder.build(), userHandle);
}
private void updateCallTracking(Call call, InCallServiceInfo info, boolean isAdd) {
int type = info.getType();
boolean hasUi = type == IN_CALL_SERVICE_TYPE_CAR_MODE_UI
|| type == IN_CALL_SERVICE_TYPE_DEFAULT_DIALER_UI;
call.maybeOnInCallServiceTrackingChanged(isAdd, hasUi);
}
private UserHandle getUserFromCall(Call call) {
// Call may never be specified, so we can fall back to using the CallManager current user.
if (call == null) {
return mCallsManager.getCurrentUserHandle();
} else {
UserHandle userFromCall = call.getAssociatedUser();
UserManager userManager = mContext.getSystemService(UserManager.class);
// Emergency call should never be blocked, so if the user associated with the target
// phone account handle user is in quiet mode, use the current user for the ecall.
// Note, that this only applies to incoming calls that are received on assigned
// sims (i.e. work sim), where the associated user would be the target phone account
// handle user.
if ((call.isEmergencyCall() || call.isInECBM())
&& (userManager.isQuietModeEnabled(userFromCall)
// We should also account for secondary/guest users where the profile may not
// necessarily be paused.
|| !userManager.isUserAdmin(mCallsManager.getCurrentUserHandle()
.getIdentifier()))) {
return mCallsManager.getCurrentUserHandle();
}
return userFromCall;
}
}
/**
* Useful for debugging purposes and called on the command line via
* an "adb shell telecom command".
*
* @return true if a particular non-ui InCallService package is bound in a call.
*/
public boolean isNonUiInCallServiceBound(String packageName) {
for (NonUIInCallServiceConnectionCollection ics : mNonUIInCallServiceConnections.values()) {
for (InCallServiceBindingConnection connection : ics.getSubConnections()) {
InCallServiceInfo serviceInfo = connection.mInCallServiceInfo;
Log.i(this, "isNonUiInCallServiceBound: found serviceInfo=[%s]", serviceInfo);
if (serviceInfo != null &&
serviceInfo.mComponentName.getPackageName().contains(packageName)) {
Log.i(this, "isNonUiInCallServiceBound: found target package");
return true;
}
}
}
return false;
}
}