Dynamic InCallService switching.

Be able to switch between in-call services UIs.

Created a tree data structure to handle switching between InCallServices
depending on the state of the system.  Tree looks like this:

           CarModeSwitchingConnection
                 |             |
          +------+             +-------+
          |                            |
    CarModeConnection          EmergencyConnection
                                       |
                                       |
                               DefaultDialerConnection

Bug: 24571147
Change-Id: I0999fad4185321d5211172aed2f1d60fe8f5fe3a
This commit is contained in:
Santos Cordon 2016-03-07 14:40:07 -08:00
parent d9d8fb6508
commit 501b9b3775
10 changed files with 726 additions and 204 deletions

View File

@ -12,7 +12,8 @@
* 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;
*/
package com.android.server.telecom;
import com.android.internal.telephony.CallerInfoAsyncQuery;

View File

@ -53,6 +53,7 @@ import com.android.internal.telephony.AsyncEmergencyContactNotifier;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.TelephonyProperties;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
import com.android.server.telecom.components.ErrorDialogActivity;
import java.util.Collection;
@ -200,7 +201,8 @@ public class CallsManager extends Call.ListenerBase
CallAudioManager.AudioServiceFactory audioServiceFactory,
BluetoothManager bluetoothManager,
WiredHeadsetManager wiredHeadsetManager,
SystemStateProvider systemStateProvider) {
SystemStateProvider systemStateProvider,
DefaultDialerManagerAdapter defaultDialerAdapter) {
mContext = context;
mLock = lock;
mContactsAsyncHelper = contactsAsyncHelper;
@ -238,7 +240,8 @@ public class CallsManager extends Call.ListenerBase
RingtoneFactory ringtoneFactory = new RingtoneFactory(this, context);
SystemVibrator systemVibrator = new SystemVibrator(context);
AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
mInCallController = new InCallController(context, mLock, this, systemStateProvider);
mInCallController = new InCallController(
context, mLock, this, systemStateProvider, defaultDialerAdapter);
mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
ringtoneFactory, systemVibrator, mInCallController);

View File

@ -46,6 +46,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.IInCallService;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.SystemStateProvider.SystemStateListener;
import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
import java.util.ArrayList;
import java.util.Collection;
@ -62,25 +63,358 @@ import java.util.concurrent.ConcurrentHashMap;
* a binding to the {@link IInCallService} (implemented by the in-call app).
*/
public final class InCallController extends CallsManagerListenerBase {
/**
* Used to bind to the in-call app and triggers the start of communication between
* this class and in-call app.
*/
private class InCallServiceConnection implements ServiceConnection {
/** {@inheritDoc} */
@Override public void onServiceConnected(ComponentName name, IBinder service) {
Log.startSession("ICSC.oSC");
Log.d(this, "onServiceConnected: %s", name);
onConnected(name, service);
Log.endSession();
public class InCallServiceConnection {
public class Listener {
public void onDisconnect(InCallServiceConnection conn) {}
}
/** {@inheritDoc} */
@Override public void onServiceDisconnected(ComponentName name) {
Log.startSession("ICSC.oSD");
Log.d(this, "onDisconnected: %s", name);
onDisconnected(name);
Log.endSession();
protected Listener mListener;
public boolean connect(Call call) { return false; }
public void disconnect() {}
public void setHasEmergency(boolean hasEmergency) {}
public void setListener(Listener l) {
mListener = l;
}
public void dump(IndentingPrintWriter pw) {}
}
private class InCallServiceBindingConnection extends InCallServiceConnection {
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.startSession("ICSBC.oSC");
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");
try {
Log.d(this, "onDisconnected: %s", name);
mIsBound = false;
onDisconnected();
} finally {
Log.endSession();
}
}
};
private final ComponentName mComponentName;
private boolean mIsConnected = false;
private boolean mIsBound = false;
public InCallServiceBindingConnection(ComponentName componentName) {
mComponentName = componentName;
}
@Override
public boolean connect(Call call) {
if (mIsConnected) {
Log.event(call, Log.Events.INFO, "Already connected, ignoring request.");
return true;
}
Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
intent.setComponent(mComponentName);
if (call != null && !call.isIncoming()){
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", mComponentName, intent);
if (mContext.bindServiceAsUser(intent, mServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
UserHandle.CURRENT)) {
Log.i(this, "It's connecting!");
mIsConnected = true;
}
return mIsConnected;
}
@Override
public void disconnect() {
if (mIsConnected) {
mContext.unbindService(mServiceConnection);
mIsConnected = false;
} else {
Log.event(null, Log.Events.INFO, "Already disconnected, ignoring request.");
}
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.append("BindingConnection [");
pw.append(mIsConnected ? "" : "not ").append("connected, ");
pw.append(mIsBound ? "" : "not ").append("bound]\n");
}
protected void onConnected(IBinder service) {
boolean shouldRemainConnected =
InCallController.this.onConnected(mComponentName, service);
if (!shouldRemainConnected) {
// Sometimes we can opt to disconnect for certain reasons, like if the
// InCallService rejected our intialization 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() {
InCallController.this.onDisconnected(mComponentName);
disconnect(); // Unbind explicitly if we get disconnected.
if (mListener != null) {
mListener.onDisconnect(InCallServiceBindingConnection.this);
}
}
}
/**
* 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) {
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(null);
}
}
}
};
public EmergencyInCallServiceConnection(
ComponentName componentName, InCallServiceConnection subConnection) {
super(componentName);
mSubConnection = subConnection;
if (mSubConnection != null) {
mSubConnection.setListener(mSubListener);
}
mIsProxying = (mSubConnection != null);
}
@Override
public boolean connect(Call call) {
mIsConnected = true;
if (mIsProxying) {
if (mSubConnection.connect(call)) {
return true;
}
// Could not connect to child, stop proxying.
mIsProxying = false;
}
// 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, "Disconnect forced!");
if (mIsProxying) {
mSubConnection.disconnect();
} else {
super.disconnect();
}
mIsConnected = false;
}
@Override
public void setHasEmergency(boolean hasEmergency) {
if (hasEmergency) {
takeControl();
}
}
@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(null); // reconnect
}
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.println("Emergency ICS Connection");
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 final 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();
}
public synchronized void setCarMode(boolean isCarMode) {
Log.i(this, "carmodechange: " + mIsCarMode + " => " + isCarMode);
if (isCarMode != mIsCarMode) {
mIsCarMode = isCarMode;
InCallServiceConnection newConnection = getCurrentConnection();
if (newConnection != mCurrentConnection) {
if (mIsConnected) {
mCurrentConnection.disconnect();
newConnection.connect(null);
}
mCurrentConnection = newConnection;
}
}
}
@Override
public boolean connect(Call call) {
if (mIsConnected) {
Log.i(this, "already connected");
return true;
} else {
if (mCurrentConnection.connect(call)) {
mIsConnected = true;
return true;
}
}
return false;
}
@Override
public void disconnect() {
if (mIsConnected) {
mCurrentConnection.disconnect();
mIsConnected = false;
} else {
Log.i(this, "already disconnected");
}
}
@Override
public void setHasEmergency(boolean hasEmergency) {
if (mDialerConnection != null) {
mDialerConnection.setHasEmergency(hasEmergency);
}
if (mCarModeConnection != null) {
mCarModeConnection.setHasEmergency(hasEmergency);
}
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.println("Car Swapping ICS");
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 boolean connect(Call call) {
for (InCallServiceBindingConnection subConnection : mSubConnections) {
subConnection.connect(call);
}
return true;
}
@Override
public void disconnect() {
for (InCallServiceBindingConnection subConnection : mSubConnections) {
subConnection.disconnect();
}
}
@Override
public void dump(IndentingPrintWriter pw) {
pw.println("Non-UI Connections:");
pw.increaseIndent();
for (InCallServiceBindingConnection subConnection : mSubConnections) {
subConnection.dump(pw);
}
pw.decreaseIndent();
}
}
@ -180,7 +514,9 @@ public final class InCallController extends CallsManagerListenerBase {
private final SystemStateListener mSystemStateListener = new SystemStateListener() {
@Override
public void onCarModeChanged(boolean isCarMode) {
// Do something when the car mode changes.
if (mInCallServiceConnection != null) {
mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
}
}
};
@ -190,15 +526,6 @@ public final class InCallController extends CallsManagerListenerBase {
private static final int IN_CALL_SERVICE_TYPE_CAR_MODE_UI = 3;
private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
/**
* Maintains a binding connection to the in-call app(s).
* ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
* load factor before resizing, 1 means we only expect a single thread to
* access the map so make only a single shard
*/
private final Map<ComponentName, InCallServiceConnection> mServiceConnections =
new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1);
/** The in-call app implementations, see {@link IInCallService}. */
private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
@ -216,13 +543,18 @@ public final class InCallController extends CallsManagerListenerBase {
private final TelecomSystem.SyncRoot mLock;
private final CallsManager mCallsManager;
private final SystemStateProvider mSystemStateProvider;
private final DefaultDialerManagerAdapter mDefaultDialerAdapter;
private CarSwappingInCallServiceConnection mInCallServiceConnection;
private NonUIInCallServiceConnectionCollection mNonUIInCallServiceConnections;
public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
SystemStateProvider systemStateProvider) {
SystemStateProvider systemStateProvider,
DefaultDialerManagerAdapter defaultDialerAdapter) {
mContext = context;
mLock = lock;
mCallsManager = callsManager;
mSystemStateProvider = systemStateProvider;
mDefaultDialerAdapter = defaultDialerAdapter;
Resources resources = mContext.getResources();
mSystemInCallComponentName = new ComponentName(
@ -379,19 +711,12 @@ public final class InCallController extends CallsManagerListenerBase {
* Unbinds an existing bound connection to the in-call app.
*/
private void unbindFromServices() {
Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator =
mServiceConnections.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<ComponentName, InCallServiceConnection> entry = iterator.next();
Log.i(this, "Unbinding from InCallService %s", entry.getKey());
try {
mContext.unbindService(entry.getValue());
} catch (Exception e) {
Log.e(this, e, "Exception while unbinding from InCallService");
}
iterator.remove();
if (isBoundToServices()) {
mInCallServiceConnection.disconnect();
mInCallServiceConnection = null;
mNonUIInCallServiceConnections.disconnect();
mNonUIInCallServiceConnections = null;
}
mInCallServices.clear();
}
/**
@ -402,13 +727,72 @@ public final class InCallController extends CallsManagerListenerBase {
*/
@VisibleForTesting
public void bindToServices(Call call) {
ComponentName inCallUIService = null;
ComponentName carModeInCallUIService = null;
List<ComponentName> nonUIInCallServices = new LinkedList<>();
InCallServiceConnection dialerInCall = null;
ComponentName defaultDialerComponent = getDefaultDialerComponent();
Log.i(this, "defaultDialer: " + defaultDialerComponent);
if (defaultDialerComponent != null &&
!defaultDialerComponent.equals(mSystemInCallComponentName)) {
dialerInCall = new InCallServiceBindingConnection(defaultDialerComponent);
}
Log.i(this, "defaultDialer: " + dialerInCall);
EmergencyInCallServiceConnection systemInCall =
new EmergencyInCallServiceConnection(mSystemInCallComponentName, dialerInCall);
systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
InCallServiceConnection carModeInCall = null;
ComponentName carModeComponent = getCarModeComponent();
if (carModeComponent != null &&
!carModeComponent.equals(mSystemInCallComponentName)) {
carModeInCall = new InCallServiceBindingConnection(carModeComponent);
}
mInCallServiceConnection =
new CarSwappingInCallServiceConnection(systemInCall, carModeInCall);
mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
mInCallServiceConnection.connect(call);
List<ComponentName> nonUIInCallComponents =
getInCallServiceComponents(null, IN_CALL_SERVICE_TYPE_NON_UI);
List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
for (ComponentName componentName : nonUIInCallComponents) {
nonUIInCalls.add(new InCallServiceBindingConnection(componentName));
}
mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(nonUIInCalls);
mNonUIInCallServiceConnections.connect(call);
}
private ComponentName getDefaultDialerComponent() {
String packageName = mDefaultDialerAdapter.getDefaultDialerApplication(
mContext, mCallsManager.getCurrentUserHandle().getIdentifier());
Log.d(this, "Default Dialer package: " + packageName);
return getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_DIALER_UI);
}
private ComponentName getCarModeComponent() {
return getInCallServiceComponent(null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
}
private ComponentName getInCallServiceComponent(String packageName, int type) {
List<ComponentName> list = getInCallServiceComponents(packageName, type);
if (list != null && !list.isEmpty()) {
return list.get(0);
}
return null;
}
private List<ComponentName> getInCallServiceComponents(String packageName, int type) {
List<ComponentName> retval = new LinkedList<>();
// Loop through all the InCallService implementations that exist in the devices;
PackageManager packageManager = mContext.getPackageManager();
Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
if (packageName != null) {
serviceIntent.setPackage(packageName);
}
PackageManager packageManager = mContext.getPackageManager();
for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
serviceIntent,
PackageManager.GET_META_DATA,
@ -416,107 +800,13 @@ public final class InCallController extends CallsManagerListenerBase {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
ComponentName componentName =
new ComponentName(serviceInfo.packageName, serviceInfo.name);
Log.v(this, "ICS: " + componentName + ", user: " + entry.targetUserId);
switch (getInCallServiceType(entry.serviceInfo, packageManager)) {
case IN_CALL_SERVICE_TYPE_DIALER_UI:
if (inCallUIService == null ||
inCallUIService.compareTo(componentName) > 0) {
inCallUIService = componentName;
}
break;
case IN_CALL_SERVICE_TYPE_SYSTEM_UI:
// skip, will be added manually
break;
case IN_CALL_SERVICE_TYPE_CAR_MODE_UI:
if (carModeInCallUIService == null ||
carModeInCallUIService.compareTo(componentName) > 0) {
carModeInCallUIService = componentName;
}
break;
case IN_CALL_SERVICE_TYPE_NON_UI:
nonUIInCallServices.add(componentName);
break;
case IN_CALL_SERVICE_TYPE_INVALID:
break;
default:
Log.w(this, "unexpected in-call service type");
break;
if (type == 0 || type == getInCallServiceType(entry.serviceInfo, packageManager)) {
retval.add(new ComponentName(serviceInfo.packageName, serviceInfo.name));
}
}
}
Log.i(this, "Car mode InCallService: %s", carModeInCallUIService);
Log.i(this, "Dialer InCallService: %s", inCallUIService);
// Adding the in-call services in order:
// (1) The carmode in-call if carmode is on.
// (2) The default-dialer in-call if not an emergency call
// (3) The system-provided in-call
List<ComponentName> orderedInCallUIServices = new LinkedList<>();
if (shouldUseCarModeUI() && carModeInCallUIService != null) {
orderedInCallUIServices.add(carModeInCallUIService);
}
if (!mCallsManager.hasEmergencyCall() && inCallUIService != null) {
orderedInCallUIServices.add(inCallUIService);
}
orderedInCallUIServices.add(mSystemInCallComponentName);
// TODO: Need to implement the fall-back logic in case the main UI in-call service rejects
// the binding request.
ComponentName inCallUIServiceToBind = orderedInCallUIServices.get(0);
if (!bindToInCallService(inCallUIServiceToBind, call, "ui")) {
Log.event(call, Log.Events.ERROR_LOG,
"InCallService system UI failed binding: " + inCallUIService);
}
mInCallUIComponentName = inCallUIServiceToBind;
// Bind to the control InCallServices
for (ComponentName componentName : nonUIInCallServices) {
bindToInCallService(componentName, call, "control");
}
}
/**
* Binds to the specified InCallService.
*/
private boolean bindToInCallService(ComponentName componentName, Call call, String tag) {
if (mInCallServices.containsKey(componentName)) {
Log.i(this, "An InCallService already exists: %s", componentName);
return true;
}
if (mServiceConnections.containsKey(componentName)) {
Log.w(this, "The service is already bound for this component %s", componentName);
return true;
}
Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
intent.setComponent(componentName);
if (call != null && !call.isIncoming()){
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 [%s] InCall %s, with %s", tag, componentName, intent);
InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
if (mContext.bindServiceAsUser(intent, inCallServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
UserHandle.CURRENT)) {
mServiceConnections.put(componentName, inCallServiceConnection);
return true;
}
return false;
return retval;
}
private boolean shouldUseCarModeUI() {
@ -560,7 +850,7 @@ public final class InCallController extends CallsManagerListenerBase {
// Check to see that it is the default dialer package
boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
DefaultDialerManager.getDefaultDialerApplication(
mDefaultDialerAdapter.getDefaultDialerApplication(
mContext, mCallsManager.getCurrentUserHandle().getIdentifier()));
boolean isUIService = serviceInfo.metaData != null &&
serviceInfo.metaData.getBoolean(
@ -583,13 +873,10 @@ public final class InCallController extends CallsManagerListenerBase {
}
private void adjustServiceBindingsForEmergency() {
if (!Objects.equals(mInCallUIComponentName, mSystemInCallComponentName)) {
// The connected UI is not the system UI, so lets check if we should switch them
// if there exists an emergency number.
if (mCallsManager.hasEmergencyCall()) {
// Lets fake a failure here in order to trigger the switch to the system UI.
onInCallServiceFailure(mInCallUIComponentName, "emergency adjust");
}
// The connected UI is not the system UI, so lets check if we should switch them
// if there exists an emergency number.
if (mCallsManager.hasEmergencyCall()) {
mInCallServiceConnection.setHasEmergency(true);
}
}
@ -600,8 +887,9 @@ public final class InCallController extends CallsManagerListenerBase {
*
* @param componentName The service {@link ComponentName}.
* @param service The {@link IInCallService} implementation.
* @return True if we successfully connected.
*/
private void onConnected(ComponentName componentName, IBinder service) {
private boolean onConnected(ComponentName componentName, IBinder service) {
Trace.beginSection("onConnected: " + componentName);
Log.i(this, "onConnected to %s", componentName);
@ -618,8 +906,7 @@ public final class InCallController extends CallsManagerListenerBase {
} catch (RemoteException e) {
Log.e(this, e, "Failed to set the in-call adapter.");
Trace.endSection();
onInCallServiceFailure(componentName, "setInCallAdapter");
return;
return false;
}
// Upon successful connection, send the state of the world to the service.
@ -644,9 +931,10 @@ public final class InCallController extends CallsManagerListenerBase {
} catch (RemoteException ignored) {
}
} else {
unbindFromServices();
return false;
}
Trace.endSection();
return true;
}
/**
@ -658,40 +946,6 @@ public final class InCallController extends CallsManagerListenerBase {
Log.i(this, "onDisconnected from %s", disconnectedComponent);
mInCallServices.remove(disconnectedComponent);
if (mServiceConnections.containsKey(disconnectedComponent)) {
// One of the services that we were bound to has unexpectedly disconnected.
onInCallServiceFailure(disconnectedComponent, "onDisconnect");
}
}
/**
* Handles non-recoverable failures by the InCallService. This method performs cleanup and
* special handling when the failure is to the UI InCallService.
*/
private void onInCallServiceFailure(ComponentName componentName, String tag) {
Log.i(this, "Cleaning up a failed InCallService [%s]: %s", tag, componentName);
// We always clean up the connections here. Even in the case where we rebind to the UI
// because binding is count based and we could end up double-bound.
mInCallServices.remove(componentName);
InCallServiceConnection serviceConnection = mServiceConnections.remove(componentName);
if (serviceConnection != null) {
// We still need to call unbind even though it disconnected.
mContext.unbindService(serviceConnection);
}
if (Objects.equals(mInCallUIComponentName, componentName)) {
if (!mCallsManager.hasAnyCalls()) {
// No calls are left anyway. Lets just disconnect all of them.
unbindFromServices();
return;
}
// Whenever the UI crashes, we automatically revert to the System UI for the
// remainder of the active calls.
mInCallUIComponentName = mSystemInCallComponentName;
bindToInCallService(mInCallUIComponentName, null, "reconnecting");
}
}
/**
@ -743,7 +997,7 @@ public final class InCallController extends CallsManagerListenerBase {
}
private boolean isBoundToServices() {
return !mInCallServices.isEmpty();
return mInCallServiceConnection != null;
}
/**
@ -759,10 +1013,10 @@ public final class InCallController extends CallsManagerListenerBase {
}
pw.decreaseIndent();
pw.println("mServiceConnections (InCalls bound):");
pw.println("ServiceConnections (InCalls bound):");
pw.increaseIndent();
for (ComponentName componentName : mServiceConnections.keySet()) {
pw.println(componentName);
if (mInCallServiceConnection != null) {
mInCallServiceConnection.dump(pw);
}
pw.decreaseIndent();
}

View File

@ -104,6 +104,7 @@ public class Log {
public static final String REMOTELY_HELD = "REMOTELY_HELD";
public static final String REMOTELY_UNHELD = "REMOTELY_UNHELD";
public static final String PULL = "PULL";
public static final String INFO = "INFO";
/**
* Maps from a request to a response. The same event could be listed as the

View File

@ -67,6 +67,7 @@ import java.util.List;
public class TelecomServiceImpl {
public interface DefaultDialerManagerAdapter {
String getDefaultDialerApplication(Context context);
String getDefaultDialerApplication(Context context, int userId);
boolean setDefaultDialerApplication(Context context, String packageName);
boolean isDefaultOrSystemDialer(Context context, String packageName);
}
@ -77,6 +78,11 @@ public class TelecomServiceImpl {
return DefaultDialerManager.getDefaultDialerApplication(context);
}
@Override
public String getDefaultDialerApplication(Context context, int userId) {
return DefaultDialerManager.getDefaultDialerApplication(context, userId);
}
@Override
public boolean setDefaultDialerApplication(Context context, String packageName) {
return DefaultDialerManager.setDefaultDialerApplication(context, packageName);

View File

@ -22,6 +22,7 @@ import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
import com.android.server.telecom.BluetoothPhoneServiceImpl.BluetoothPhoneServiceImplFactory;
import com.android.server.telecom.CallAudioManager.AudioServiceFactory;
import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
import android.Manifest;
import android.content.BroadcastReceiver;
@ -173,6 +174,9 @@ public final class TelecomSystem {
mMissedCallNotifier = missedCallNotifierImplFactory
.makeMissedCallNotifierImpl(mContext, mPhoneAccountRegistrar);
DefaultDialerManagerAdapter defaultDialerAdapter =
new TelecomServiceImpl.DefaultDialerManagerAdapterImpl();
mCallsManager = new CallsManager(
mContext,
mLock,
@ -186,7 +190,8 @@ public final class TelecomSystem {
audioServiceFactory,
bluetoothManager,
wiredHeadsetManager,
systemStateProvider);
systemStateProvider,
defaultDialerAdapter);
mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
@ -214,7 +219,7 @@ public final class TelecomSystem {
return new UserCallIntentProcessor(context, userHandle);
}
},
new TelecomServiceImpl.DefaultDialerManagerAdapterImpl(),
defaultDialerAdapter,
new TelecomServiceImpl.SubscriptionManagerAdapterImpl(),
mLock);
}

View File

@ -54,7 +54,7 @@
<service android:name="com.android.server.telecom.testapps.TestInCallServiceImpl"
android:process="com.android.server.telecom.testapps.TestInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE" >
<meta-data android:name="android.telecom.IN_CALL_SERVICE_CAR_MODE_UI" android:value="true"/>
<meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true"/>
<intent-filter>
<action android:name="android.telecom.InCallService"/>
</intent-filter>

View File

@ -148,7 +148,10 @@ public class TestCallList extends Call.Listener {
}
public void clearCalls() {
mCalls.clear();
for (Call call : new LinkedList<Call>(mCalls)) {
removeCall(call);
}
for (Call call : mVideoCallListeners.keySet()) {
if (call.getVideoCall() != null) {
call.getVideoCall().destroy();

View File

@ -16,14 +16,17 @@
package com.android.server.telecom.tests;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.IBinder;
import android.os.UserHandle;
import android.telecom.ConnectionService;
import android.telecom.InCallService;
@ -32,6 +35,8 @@ import android.telecom.TelecomManager;
import android.test.mock.MockContext;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.internal.telecom.IInCallAdapter;
import com.android.internal.telecom.IInCallService;
import com.android.server.telecom.BluetoothHeadsetProxy;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
@ -39,6 +44,7 @@ import com.android.server.telecom.InCallController;
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.R;
import com.android.server.telecom.SystemStateProvider;
import com.android.server.telecom.TelecomServiceImpl.DefaultDialerManagerAdapter;
import com.android.server.telecom.TelecomSystem;
import org.mockito.ArgumentCaptor;
@ -72,10 +78,13 @@ public class InCallControllerTests extends TelecomTestCase {
@Mock Call mMockCall;
@Mock Resources mMockResources;
@Mock MockContext mMockContext;
@Mock DefaultDialerManagerAdapter mMockDefaultDialerAdapter;
private static final int CURRENT_USER_ID = 900973;
private static final String DEF_PKG = "defpkg";
private static final String DEF_CLASS = "defcls";
private static final String SYS_PKG = "syspkg";
private static final String SYS_CLASS = "syscls";
private static final PhoneAccountHandle PA_HANDLE =
new PhoneAccountHandle(new ComponentName("pa_pkg", "pa_cls"), "pa_id");
@ -88,10 +97,10 @@ public class InCallControllerTests extends TelecomTestCase {
super.setUp();
MockitoAnnotations.initMocks(this);
doReturn(mMockResources).when(mMockContext).getResources();
doReturn(DEF_PKG).when(mMockResources).getString(R.string.ui_default_package);
doReturn(DEF_CLASS).when(mMockResources).getString(R.string.incall_default_class);
doReturn(SYS_PKG).when(mMockResources).getString(R.string.ui_default_package);
doReturn(SYS_CLASS).when(mMockResources).getString(R.string.incall_default_class);
mInCallController = new InCallController(mMockContext, mLock, mMockCallsManager,
mMockSystemStateProvider);
mMockSystemStateProvider, mMockDefaultDialerAdapter);
}
@Override
@ -121,8 +130,8 @@ public class InCallControllerTests extends TelecomTestCase {
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName());
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
assertNull(bindIntent.getExtras());
}
@ -151,6 +160,74 @@ public class InCallControllerTests extends TelecomTestCase {
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
assertEquals(PA_HANDLE, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
assertEquals(callExtras, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS));
}
@MediumTest
public void testBindToService_DefaultDialer_NoEmergency() throws Exception {
Bundle callExtras = new Bundle();
callExtras.putBoolean("whatever", true);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
.thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
when(mMockPackageManager.queryIntentServicesAsUser(
any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
.thenReturn(new LinkedList<ResolveInfo>() {{
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = DEF_PKG;
serviceInfo.name = DEF_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
serviceInfo.metaData = new Bundle();
serviceInfo.metaData.putBoolean(
TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
}});
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = SYS_PKG;
serviceInfo.name = SYS_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
}});
}});
mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
queryIntentCaptor.capture(),
eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
// Verify call for default dialer InCallService
assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
// Verify call for car-mode InCallService
assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
// Verify call for non-UI InCallServices
assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
@ -160,4 +237,171 @@ public class InCallControllerTests extends TelecomTestCase {
assertEquals(callExtras, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS));
}
@MediumTest
public void testBindToService_SystemDialer_Emergency() throws Exception {
Bundle callExtras = new Bundle();
callExtras.putBoolean("whatever", true);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.hasEmergencyCall()).thenReturn(true);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
.thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT))).thenReturn(true);
Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
when(mMockPackageManager.queryIntentServicesAsUser(
any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
.thenReturn(new LinkedList<ResolveInfo>() {{
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = DEF_PKG;
serviceInfo.name = DEF_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
serviceInfo.metaData = new Bundle();
serviceInfo.metaData.putBoolean(
TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
}});
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = SYS_PKG;
serviceInfo.name = SYS_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
}});
}});
mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
queryIntentCaptor.capture(),
eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
// Verify call for default dialer InCallService
assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
// Verify call for car-mode InCallService
assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
// Verify call for non-UI InCallServices
assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(1)).bindServiceAsUser(
bindIntentCaptor.capture(),
any(ServiceConnection.class),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
assertEquals(PA_HANDLE, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
assertEquals(callExtras, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS));
}
@MediumTest
public void testBindToService_DefaultDialer_FallBackToSystem() throws Exception {
Bundle callExtras = new Bundle();
callExtras.putBoolean("whatever", true);
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
when(mMockCall.isIncoming()).thenReturn(false);
when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
when(mMockCall.getIntentExtras()).thenReturn(callExtras);
when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
.thenReturn(DEF_PKG);
when(mMockContext.bindServiceAsUser(
any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)))
.thenReturn(true);
Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
when(mMockPackageManager.queryIntentServicesAsUser(
any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
.thenReturn(new LinkedList<ResolveInfo>() {{
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = DEF_PKG;
serviceInfo.name = DEF_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
serviceInfo.metaData = new Bundle();
serviceInfo.metaData.putBoolean(
TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
}});
add(new ResolveInfo() {{
serviceInfo = new ServiceInfo();
serviceInfo.packageName = SYS_PKG;
serviceInfo.name = SYS_CLASS;
serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
}});
}});
mInCallController.bindToServices(mMockCall);
// Query for the different InCallServices
ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
queryIntentCaptor.capture(),
eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
// Verify call for default dialer InCallService
assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
// Verify call for car-mode InCallService
assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
// Verify call for non-UI InCallServices
assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> serviceConnectionCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
verify(mMockContext, times(1)).bindServiceAsUser(
bindIntentCaptor.capture(),
serviceConnectionCaptor.capture(),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT));
Intent bindIntent = bindIntentCaptor.getValue();
assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName());
assertEquals(PA_HANDLE, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE));
assertEquals(callExtras, bindIntent.getExtras().getParcelable(
TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS));
// We have a ServiceConnection for the default dialer, lets start the connection, and then
// simulate a crash so that we fallback to system.
ServiceConnection serviceConnection = serviceConnectionCaptor.getValue();
ComponentName defDialerComponentName = new ComponentName(DEF_PKG, DEF_CLASS);
IBinder mockBinder = mock(IBinder.class);
IInCallService mockInCallService = mock(IInCallService.class);
when(mockBinder.queryLocalInterface(anyString())).thenReturn(mockInCallService);
// Start the connection with IInCallService
serviceConnection.onServiceConnected(defDialerComponentName, mockBinder);
verify(mockInCallService).setInCallAdapter(any(IInCallAdapter.class));
// Now crash the damn thing!
serviceConnection.onServiceDisconnected(defDialerComponentName);
ArgumentCaptor<Intent> bindIntentCaptor2 = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext, times(2)).bindServiceAsUser(
bindIntentCaptor2.capture(),
any(ServiceConnection.class),
eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
eq(UserHandle.CURRENT));
bindIntent = bindIntentCaptor2.getValue();
assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
}
}

View File

@ -103,6 +103,11 @@ public class TelecomServiceImplTest extends TelecomTestCase {
return null;
}
@Override
public String getDefaultDialerApplication(Context context, int userId) {
return null;
}
@Override
public boolean setDefaultDialerApplication(Context context, String packageName) {
return false;