/* * 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 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> mImsFeatures; private IImsServiceController mIImsServiceController; // Easier for testing. private IBinder mImsServiceControllerBinder; private ImsServiceConnection mImsServiceConnection; private ImsDeathRecipient mImsDeathRecipient; private Set mImsStatusCallbacks = new HashSet<>(); // Only added or removed, never accessed on purpose. private Set 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> 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> newImsFeatures) throws RemoteException { synchronized (mLock) { if (mIsBound) { // add features to service. HashSet> newFeatures = new HashSet<>(newImsFeatures); newFeatures.removeAll(mImsFeatures); for (Pair i : newFeatures) { addImsServiceFeature(i); } // remove old features HashSet> oldFeatures = new HashSet<>(mImsFeatures); oldFeatures.removeAll(newImsFeatures); for (Pair 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 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 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 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 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 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 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; } } }