Exposing in-call API to other implementing classes. (2/4)
- Modifying InCallController to look for and bind to all implementors of
the InCallService (not just the the InCallUI's).
- Added TestInCallServiceImpl to test binding to multiple InCallServices.
- Added new CONTROL_INCALL_EXPERIENCE system permission.
Bug: 16133960
Bug: 17007163
Change-Id: I4630dfd31f0c86228189c89902292856bd255642
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 00ed147..4f022c0 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -16,28 +16,36 @@
package com.android.telecomm;
+import android.Manifest;
import android.app.PendingIntent;
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.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telecomm.AudioState;
+import android.telecomm.CallState;
+import android.telecomm.InCallService;
+import android.telecomm.ParcelableCall;
import android.telecomm.PhoneCapabilities;
import android.telecomm.PropertyPresentation;
-import android.telecomm.CallState;
-import android.telecomm.ParcelableCall;
+import android.util.ArrayMap;
import com.android.internal.telecomm.IInCallService;
import com.google.common.collect.ImmutableCollection;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
@@ -52,12 +60,12 @@
private class InCallServiceConnection implements ServiceConnection {
/** {@inheritDoc} */
@Override public void onServiceConnected(ComponentName name, IBinder service) {
- onConnected(service);
+ onConnected(name, service);
}
/** {@inheritDoc} */
@Override public void onServiceDisconnected(ComponentName name) {
- onDisconnected();
+ onDisconnected(name);
}
}
@@ -99,11 +107,13 @@
@Override
public void onStartActivityFromInCall(Call call, PendingIntent intent) {
- if (mInCallService != null) {
+ if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling startActivity, intent: %s", intent);
- try {
- mInCallService.startActivity(mCallIdMapper.getCallId(call), intent);
- } catch (RemoteException ignored) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.startActivity(mCallIdMapper.getCallId(call), intent);
+ } catch (RemoteException ignored) {
+ }
}
}
}
@@ -119,29 +129,45 @@
}
};
- /** Maintains a binding connection to the in-call app. */
- private final InCallServiceConnection mConnection = new InCallServiceConnection();
+ /**
+ * 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 implementation, see {@link IInCallService}. */
- private IInCallService mInCallService;
+ /** The in-call app implementations, see {@link IInCallService}. */
+ private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall");
- IInCallService getService() {
- return mInCallService;
+ /** The {@link ComponentName} of the default InCall UI */
+ private ComponentName mInCallComponentName;
+
+ public InCallController() {
+ Context context = TelecommApp.getInstance();
+ Resources resources = context.getResources();
+
+ mInCallComponentName = new ComponentName(
+ resources.getString(R.string.ui_default_package),
+ resources.getString(R.string.incall_default_class));
}
@Override
public void onCallAdded(Call call) {
- if (mInCallService == null) {
+ if (mInCallServices.isEmpty()) {
bind();
} else {
Log.i(this, "Adding call: %s", call);
- if (mCallIdMapper.getCallId(call) == null) {
- mCallIdMapper.addCall(call);
- call.addListener(mCallListener);
+ // Track the call if we don't already know about it.
+ addCall(call);
+
+ ParcelableCall parcelableCall = toParcelableCall(call);
+ for (IInCallService inCallService : mInCallServices.values()) {
try {
- mInCallService.addCall(toParcelableCall(call));
+ inCallService.addCall(parcelableCall);
} catch (RemoteException ignored) {
}
}
@@ -173,22 +199,26 @@
@Override
public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
- if (mInCallService != null) {
+ if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
newAudioState);
- try {
- mInCallService.onAudioStateChanged(newAudioState);
- } catch (RemoteException ignored) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.onAudioStateChanged(newAudioState);
+ } catch (RemoteException ignored) {
+ }
}
}
}
void onPostDialWait(Call call, String remaining) {
- if (mInCallService != null) {
+ if (!mInCallServices.isEmpty()) {
Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
- try {
- mInCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
- } catch (RemoteException ignored) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
+ } catch (RemoteException ignored) {
+ }
}
}
}
@@ -200,10 +230,12 @@
}
void bringToForeground(boolean showDialpad) {
- if (mInCallService != null) {
- try {
- mInCallService.bringToForeground(showDialpad);
- } catch (RemoteException ignored) {
+ if (!mInCallServices.isEmpty()) {
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.bringToForeground(showDialpad);
+ } catch (RemoteException ignored) {
+ }
}
} else {
Log.w(this, "Asking to bring unbound in-call UI to foreground.");
@@ -215,10 +247,12 @@
*/
private void unbind() {
ThreadUtil.checkOnMainThread();
- if (mInCallService != null) {
+ if (!mInCallServices.isEmpty()) {
Log.i(this, "Unbinding from InCallService");
- TelecommApp.getInstance().unbindService(mConnection);
- mInCallService = null;
+ for (InCallServiceConnection connection : mServiceConnections.values()) {
+ TelecommApp.getInstance().unbindService(connection);
+ }
+ mInCallServices.clear();
}
}
@@ -228,22 +262,46 @@
*/
private void bind() {
ThreadUtil.checkOnMainThread();
- if (mInCallService == null) {
+ if (mInCallServices.isEmpty()) {
+ mServiceConnections.clear();
Context context = TelecommApp.getInstance();
- Resources resources = context.getResources();
- ComponentName component = new ComponentName(
- resources.getString(R.string.ui_default_package),
- resources.getString(R.string.incall_default_class));
- Log.i(this, "Attempting to bind to InCallService: %s", component);
+ PackageManager packageManager = TelecommApp.getInstance().getPackageManager();
+ Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
- Intent serviceIntent = new Intent(IInCallService.class.getName());
- serviceIntent.setComponent(component);
+ for (ResolveInfo entry : packageManager.queryIntentServices(intent, 0)) {
+ ServiceInfo serviceInfo = entry.serviceInfo;
+ if (serviceInfo != null) {
+ boolean hasServiceBindPermission = serviceInfo.permission != null &&
+ serviceInfo.permission.equals(
+ Manifest.permission.BIND_INCALL_SERVICE);
+ boolean hasControlInCallPermission = packageManager.checkPermission(
+ Manifest.permission.CONTROL_INCALL_EXPERIENCE,
+ serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
- if (!context.bindServiceAsUser(serviceIntent, mConnection, Context.BIND_AUTO_CREATE,
- UserHandle.CURRENT)) {
- Log.w(this, "Could not connect to the in-call app (%s)", component);
+ if (!hasServiceBindPermission) {
+ Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
+ serviceInfo.packageName);
+ continue;
+ }
- // TODO: Implement retry or fall-back-to-default logic.
+ if (!hasControlInCallPermission) {
+ Log.w(this,
+ "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " +
+ serviceInfo.packageName);
+ continue;
+ }
+
+ Log.i(this, "Attempting to bind to InCall " + serviceInfo.packageName);
+ InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
+ ComponentName componentName = new ComponentName(serviceInfo.packageName,
+ serviceInfo.name);
+ intent.setComponent(componentName);
+
+ if (context.bindServiceAsUser(intent, inCallServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
+ mServiceConnections.put(componentName, inCallServiceConnection);
+ }
+ }
}
}
}
@@ -253,26 +311,34 @@
* 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 componentName The service {@link ComponentName}.
* @param service The {@link IInCallService} implementation.
*/
- private void onConnected(IBinder service) {
+ private void onConnected(ComponentName componentName, IBinder service) {
ThreadUtil.checkOnMainThread();
- mInCallService = IInCallService.Stub.asInterface(service);
+
+ IInCallService inCallService = IInCallService.Stub.asInterface(service);
try {
- mInCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
+ inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
mCallIdMapper));
+ mInCallServices.put(componentName, inCallService);
} catch (RemoteException e) {
Log.e(this, e, "Failed to set the in-call adapter.");
- mInCallService = null;
return;
}
- // Upon successful connection, send the state of the world to the in-call app.
+ // Upon successful connection, send the state of the world to the service.
ImmutableCollection<Call> calls = CallsManager.getInstance().getCalls();
if (!calls.isEmpty()) {
for (Call call : calls) {
- onCallAdded(call);
+ try {
+ // Track the call if we don't already know about it.
+ addCall(call);
+
+ inCallService.addCall(toParcelableCall(call));
+ } catch (RemoteException ignored) {
+ }
}
onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
} else {
@@ -281,20 +347,50 @@
}
/**
- * Cleans up the instance of in-call app after the service has been unbound.
+ * Cleans up an instance of in-call app after the service has been unbound.
+ *
+ * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
*/
- private void onDisconnected() {
+ private void onDisconnected(ComponentName disconnectedComponent) {
ThreadUtil.checkOnMainThread();
- mInCallService = null;
+ if (mInCallServices.containsKey(disconnectedComponent)) {
+ mInCallServices.remove(disconnectedComponent);
+ }
+
+ // If the default in-call UI has disconnected, disconnect all calls and un-bind all other
+ // InCallService implementations.
+ if (disconnectedComponent.equals(mInCallComponentName)) {
+ Log.i(this, "In-call UI %s disconnected.", disconnectedComponent);
+ CallsManager.getInstance().disconnectAllCalls();
+
+ // Iterate through the in-call services, removing them as they are un-bound.
+ Iterator<Map.Entry<ComponentName, IInCallService>> it =
+ mInCallServices.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<ComponentName, IInCallService> entry = it.next();
+ ComponentName componentName = entry.getKey();
+
+ InCallServiceConnection connection = mServiceConnections.remove(componentName);
+ it.remove();
+ if (connection == null) {
+ continue;
+ }
+
+ Log.i(this, "Unbinding other InCallService %s", componentName);
+ TelecommApp.getInstance().unbindService(connection);
+ }
+ }
}
private void updateCall(Call call) {
- if (mInCallService != null) {
- try {
- ParcelableCall parcelableCall = toParcelableCall(call);
- Log.d(this, "updateCall %s ==> %s", call, parcelableCall);
- mInCallService.updateCall(parcelableCall);
- } catch (RemoteException ignored) {
+ if (!mInCallServices.isEmpty()) {
+ ParcelableCall parcelableCall = toParcelableCall(call);
+ Log.v(this, "updateCall %s ==> %s", call, parcelableCall);
+ for (IInCallService inCallService : mInCallServices.values()) {
+ try {
+ inCallService.updateCall(parcelableCall);
+ } catch (RemoteException ignored) {
+ }
}
}
}
@@ -372,4 +468,15 @@
conferenceableCallIds,
call.getExtras());
}
+
+ /**
+ * Adds the call to the list of calls tracked by the {@link InCallController}.
+ * @param call The call to add.
+ */
+ private void addCall(Call call) {
+ if (mCallIdMapper.getCallId(call) == null) {
+ mCallIdMapper.addCall(call);
+ call.addListener(mCallListener);
+ }
+ }
}