blob: 5257dad59e0a506912c1db5f23d087333a4303e1 [file] [log] [blame]
/*
* Copyright (C) 2017 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.internal.telephony.ims;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.IPackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import android.util.Pair;
import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.ims.internal.IImsServiceController;
import com.android.ims.internal.IImsServiceFeatureListener;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* Manages the Binding lifecycle of one ImsService as well as the relevant ImsFeatures that the
* ImsService will support.
*
* When the ImsService is first bound, {@link IImsServiceController#createImsFeature} will be
* called
* on each feature that the service supports. For each ImsFeature that is created,
* {@link ImsServiceControllerCallbacks#imsServiceFeatureCreated} will be called to notify the
* listener that the ImsService now supports that feature.
*
* When {@link #changeImsServiceFeatures} is called with a set of features that is different from
* the original set, {@link IImsServiceController#createImsFeature} and
* {@link IImsServiceController#removeImsFeature} will be called for each feature that is
* created/removed.
*/
public class ImsServiceController {
class ImsDeathRecipient implements IBinder.DeathRecipient {
private ComponentName mComponentName;
ImsDeathRecipient(ComponentName name) {
mComponentName = name;
}
@Override
public void binderDied() {
Log.e(LOG_TAG, "ImsService(" + mComponentName + ") died. Restarting...");
notifyAllFeaturesRemoved();
cleanUpService();
startDelayedRebindToService();
}
}
class ImsServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
mIsBound = true;
mIsBinding = false;
grantPermissionsToService();
Log.d(LOG_TAG, "ImsService(" + name + "): onServiceConnected with binder: "
+ service);
if (service != null) {
mImsDeathRecipient = new ImsDeathRecipient(name);
try {
service.linkToDeath(mImsDeathRecipient, 0);
mImsServiceControllerBinder = service;
mIImsServiceController = IImsServiceController.Stub.asInterface(service);
// create all associated features in the ImsService
for (Pair<Integer, Integer> i : mImsFeatures) {
addImsServiceFeature(i);
}
} catch (RemoteException e) {
mIsBound = false;
mIsBinding = false;
// Remote exception means that the binder already died.
if (mImsDeathRecipient != null) {
mImsDeathRecipient.binderDied();
}
Log.e(LOG_TAG, "ImsService(" + name + ") RemoteException:"
+ e.getMessage());
}
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
mIsBinding = false;
}
if (mIImsServiceController != null) {
mImsServiceControllerBinder.unlinkToDeath(mImsDeathRecipient, 0);
}
notifyAllFeaturesRemoved();
cleanUpService();
Log.w(LOG_TAG, "ImsService(" + name + "): onServiceDisconnected. Rebinding...");
startDelayedRebindToService();
}
}
/**
* Defines callbacks that are used by the ImsServiceController to notify when an ImsService
* has created or removed a new feature as well as the associated ImsServiceController.
*/
public interface ImsServiceControllerCallbacks {
/**
* Called by ImsServiceController when a new feature has been created.
*/
void imsServiceFeatureCreated(int slotId, int feature, ImsServiceController controller);
/**
* Called by ImsServiceController when a new feature has been removed.
*/
void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller);
}
/**
* Returns the currently defined rebind retry timeout. Used for testing.
*/
@VisibleForTesting
public interface RebindRetry {
/**
* Return a long in ms indiciating how long the ImsServiceController should wait before
* rebinding.
*/
long getRetryTimeout();
}
private static final String LOG_TAG = "ImsServiceController";
private static final int REBIND_RETRY_TIME = 5000;
private final Context mContext;
private final ComponentName mComponentName;
private final Object mLock = new Object();
private final HandlerThread mHandlerThread = new HandlerThread("ImsServiceControllerHandler");
private final IPackageManager mPackageManager;
private ImsServiceControllerCallbacks mCallbacks;
private Handler mHandler;
private boolean mIsBound = false;
private boolean mIsBinding = false;
// Set of a pair of slotId->feature
private HashSet<Pair<Integer, Integer>> mImsFeatures;
private IImsServiceController mIImsServiceController;
// Easier for testing.
private IBinder mImsServiceControllerBinder;
private ImsServiceConnection mImsServiceConnection;
private ImsDeathRecipient mImsDeathRecipient;
private Set<IImsServiceFeatureListener> mImsStatusCallbacks = new HashSet<>();
// Only added or removed, never accessed on purpose.
private Set<ImsFeatureStatusCallback> mFeatureStatusCallbacks = new HashSet<>();
/**
* Container class for the IImsFeatureStatusCallback callback implementation. This class is
* never used directly, but we need to keep track of the IImsFeatureStatusCallback
* implementations explicitly.
*/
private class ImsFeatureStatusCallback {
private int mSlotId;
private int mFeatureType;
private final IImsFeatureStatusCallback mCallback = new IImsFeatureStatusCallback.Stub() {
@Override
public void notifyImsFeatureStatus(int featureStatus) throws RemoteException {
Log.i(LOG_TAG, "notifyImsFeatureStatus: slot=" + mSlotId + ", feature="
+ mFeatureType + ", status=" + featureStatus);
sendImsFeatureStatusChanged(mSlotId, mFeatureType, featureStatus);
}
};
ImsFeatureStatusCallback(int slotId, int featureType) {
mSlotId = slotId;
mFeatureType = featureType;
}
public IImsFeatureStatusCallback getCallback() {
return mCallback;
}
}
// Retry the bind to the ImsService that has died after mRebindRetry timeout.
private Runnable mRestartImsServiceRunnable = new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (mIsBound) {
return;
}
bind(mImsFeatures);
}
}
};
private RebindRetry mRebindRetry = () -> REBIND_RETRY_TIME;
@VisibleForTesting
public void setRebindRetryTime(RebindRetry retry) {
mRebindRetry = retry;
}
@VisibleForTesting
public Handler getHandler() {
return mHandler;
}
public ImsServiceController(Context context, ComponentName componentName,
ImsServiceControllerCallbacks callbacks) {
mContext = context;
mComponentName = componentName;
mCallbacks = callbacks;
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
}
@VisibleForTesting
// Creating a new HandlerThread and background handler for each test causes a segfault, so for
// testing, use a handler supplied by the testing system.
public ImsServiceController(Context context, ComponentName componentName,
ImsServiceControllerCallbacks callbacks, Handler testHandler) {
mContext = context;
mComponentName = componentName;
mCallbacks = callbacks;
mHandler = testHandler;
mPackageManager = null;
}
/**
* Sends request to bind to ImsService designated by the {@ComponentName} with the feature set
* imsFeatureSet
*
* @param imsFeatureSet a Set of Pairs that designate the slotId->featureId that need to be
* created once the service is bound.
* @return {@link true} if the service is in the process of being bound, {@link false} if it
* has failed.
*/
public boolean bind(HashSet<Pair<Integer, Integer>> imsFeatureSet) {
synchronized (mLock) {
// Remove pending rebind retry
mHandler.removeCallbacks(mRestartImsServiceRunnable);
if (!mIsBound && !mIsBinding) {
mIsBinding = true;
mImsFeatures = imsFeatureSet;
Intent imsServiceIntent = new Intent(ImsResolver.SERVICE_INTERFACE).setComponent(
mComponentName);
mImsServiceConnection = new ImsServiceConnection();
int serviceFlags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_IMPORTANT;
Log.i(LOG_TAG, "Binding ImsService:" + mComponentName);
try {
return mContext.bindService(imsServiceIntent, mImsServiceConnection,
serviceFlags);
} catch (Exception e) {
Log.e(LOG_TAG, "Error binding (" + mComponentName + ") with exception: "
+ e.getMessage());
return false;
}
} else {
return false;
}
}
}
/**
* Calls {@link IImsServiceController#removeImsFeature} on all features that the
* ImsService supports and then unbinds the service.
*/
public void unbind() throws RemoteException {
synchronized (mLock) {
// Remove pending rebind retry
mHandler.removeCallbacks(mRestartImsServiceRunnable);
if (mImsServiceConnection == null || mImsDeathRecipient == null) {
return;
}
// Clean up all features
changeImsServiceFeatures(new HashSet<>());
removeImsServiceFeatureListener();
mImsServiceControllerBinder.unlinkToDeath(mImsDeathRecipient, 0);
Log.i(LOG_TAG, "Unbinding ImsService: " + mComponentName);
mContext.unbindService(mImsServiceConnection);
cleanUpService();
}
}
/**
* Finds the difference between the set of features that the ImsService has active and the new
* set defined in newImsFeatures. For every feature that is added,
* {@link IImsServiceController#createImsFeature} is called on the service. For every ImsFeature
* that is removed, {@link IImsServiceController#removeImsFeature} is called.
*/
public void changeImsServiceFeatures(HashSet<Pair<Integer, Integer>> newImsFeatures)
throws RemoteException {
synchronized (mLock) {
if (mIsBound) {
// add features to service.
HashSet<Pair<Integer, Integer>> newFeatures = new HashSet<>(newImsFeatures);
newFeatures.removeAll(mImsFeatures);
for (Pair<Integer, Integer> i : newFeatures) {
addImsServiceFeature(i);
}
// remove old features
HashSet<Pair<Integer, Integer>> oldFeatures = new HashSet<>(mImsFeatures);
oldFeatures.removeAll(newImsFeatures);
for (Pair<Integer, Integer> i : oldFeatures) {
removeImsServiceFeature(i);
}
}
Log.i(LOG_TAG, "Features changed (" + mImsFeatures + "->" + newImsFeatures + ") for "
+ "ImsService: " + mComponentName);
mImsFeatures = newImsFeatures;
}
}
@VisibleForTesting
public IImsServiceController getImsServiceController() {
return mIImsServiceController;
}
@VisibleForTesting
public IBinder getImsServiceControllerBinder() {
return mImsServiceControllerBinder;
}
public ComponentName getComponentName() {
return mComponentName;
}
/**
* Add a callback to ImsManager that signals a new feature that the ImsServiceProxy can handle.
*/
public void addImsServiceFeatureListener(IImsServiceFeatureListener callback) {
synchronized (mLock) {
mImsStatusCallbacks.add(callback);
}
}
private void removeImsServiceFeatureListener() {
synchronized (mLock) {
mImsStatusCallbacks.clear();
}
}
// Only add a new rebind if there are no pending rebinds waiting.
private void startDelayedRebindToService() {
if (!mHandler.hasCallbacks(mRestartImsServiceRunnable)) {
mHandler.postDelayed(mRestartImsServiceRunnable, mRebindRetry.getRetryTimeout());
}
}
// Grant runtime permissions to ImsService. PackageManager ensures that the ImsService is
// system/signed before granting permissions.
private void grantPermissionsToService() {
Log.i(LOG_TAG, "Granting Runtime permissions to:" + getComponentName());
String[] pkgToGrant = {mComponentName.getPackageName()};
try {
if (mPackageManager != null) {
mPackageManager.grantDefaultPermissionsToEnabledImsServices(pkgToGrant,
mContext.getUserId());
}
} catch (RemoteException e) {
Log.w(LOG_TAG, "Unable to grant permissions, binder died.");
}
}
private void sendImsFeatureCreatedCallback(int slot, int feature) {
synchronized (mLock) {
for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
i.hasNext(); ) {
IImsServiceFeatureListener callbacks = i.next();
try {
callbacks.imsFeatureCreated(slot, feature);
} catch (RemoteException e) {
// binder died, remove callback.
Log.w(LOG_TAG, "sendImsFeatureCreatedCallback: Binder died, removing "
+ "callback. Exception:" + e.getMessage());
i.remove();
}
}
}
}
private void sendImsFeatureRemovedCallback(int slot, int feature) {
synchronized (mLock) {
for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
i.hasNext(); ) {
IImsServiceFeatureListener callbacks = i.next();
try {
callbacks.imsFeatureRemoved(slot, feature);
} catch (RemoteException e) {
// binder died, remove callback.
Log.w(LOG_TAG, "sendImsFeatureRemovedCallback: Binder died, removing "
+ "callback. Exception:" + e.getMessage());
i.remove();
}
}
}
}
private void sendImsFeatureStatusChanged(int slot, int feature, int status) {
synchronized (mLock) {
for (Iterator<IImsServiceFeatureListener> i = mImsStatusCallbacks.iterator();
i.hasNext(); ) {
IImsServiceFeatureListener callbacks = i.next();
try {
callbacks.imsStatusChanged(slot, feature, status);
} catch (RemoteException e) {
// binder died, remove callback.
Log.w(LOG_TAG, "sendImsFeatureStatusChanged: Binder died, removing "
+ "callback. Exception:" + e.getMessage());
i.remove();
}
}
}
}
// This method should only be called when synchronized on mLock
private void addImsServiceFeature(Pair<Integer, Integer> featurePair) throws RemoteException {
if (mIImsServiceController == null || mCallbacks == null) {
Log.w(LOG_TAG, "addImsServiceFeature called with null values.");
return;
}
ImsFeatureStatusCallback c = new ImsFeatureStatusCallback(featurePair.first,
featurePair.second);
mFeatureStatusCallbacks.add(c);
mIImsServiceController.createImsFeature(featurePair.first, featurePair.second,
c.getCallback());
// Signal ImsResolver to change supported ImsFeatures for this ImsServiceController
mCallbacks.imsServiceFeatureCreated(featurePair.first, featurePair.second, this);
// Send callback to ImsServiceProxy to change supported ImsFeatures
sendImsFeatureCreatedCallback(featurePair.first, featurePair.second);
}
// This method should only be called when synchronized on mLock
private void removeImsServiceFeature(Pair<Integer, Integer> featurePair)
throws RemoteException {
if (mIImsServiceController == null || mCallbacks == null) {
Log.w(LOG_TAG, "removeImsServiceFeature called with null values.");
return;
}
ImsFeatureStatusCallback callbackToRemove = mFeatureStatusCallbacks.stream().filter(c ->
c.mSlotId == featurePair.first && c.mFeatureType == featurePair.second)
.findFirst().orElse(null);
// Remove status callbacks from list.
if (callbackToRemove != null) {
mFeatureStatusCallbacks.remove(callbackToRemove);
}
mIImsServiceController.removeImsFeature(featurePair.first, featurePair.second,
(callbackToRemove != null ? callbackToRemove.getCallback() : null));
// Signal ImsResolver to change supported ImsFeatures for this ImsServiceController
mCallbacks.imsServiceFeatureRemoved(featurePair.first, featurePair.second, this);
// Send callback to ImsServiceProxy to change supported ImsFeatures
// Ensure that ImsServiceProxy callback occurs after ImsResolver callback. If an
// ImsManager requests the ImsService while it is being removed in ImsResolver, this
// callback will clean it up after.
sendImsFeatureRemovedCallback(featurePair.first, featurePair.second);
}
private void notifyAllFeaturesRemoved() {
if (mCallbacks == null) {
Log.w(LOG_TAG, "notifyAllFeaturesRemoved called with invalid callbacks.");
return;
}
synchronized (mLock) {
for (Pair<Integer, Integer> feature : mImsFeatures) {
mCallbacks.imsServiceFeatureRemoved(feature.first, feature.second, this);
sendImsFeatureRemovedCallback(feature.first, feature.second);
}
}
}
private void cleanUpService() {
synchronized (mLock) {
mImsDeathRecipient = null;
mImsServiceConnection = null;
mImsServiceControllerBinder = null;
mIImsServiceController = null;
mIsBound = false;
}
}
}