Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 31ee2c0c authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "Introduces ImsResolver and ImsServiceController Classes"

parents 73ab9b1b ebe73954
Loading
Loading
Loading
Loading
+696 −0

File added.

Preview size limit exceeded, changes collapsed.

+430 −0
Original line number Diff line number Diff line
/*
 * 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.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. Cleaning up.");
            notifyAllFeaturesRemoved();
            cleanUpService();
            mHandler.postDelayed(mRestartImsServiceRunnable, mRebindRetry.getRetryTimeout());
        }
    }

    class ImsServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            if (service != null) {
                synchronized (mLock) {
                    mImsDeathRecipient = new ImsDeathRecipient(name);
                    try {
                        service.linkToDeath(mImsDeathRecipient, 0);
                        mImsServiceControllerBinder = service;
                        mIImsServiceController = IImsServiceController.Stub.asInterface(service);
                        mIsBound = true;
                        mIsBinding = false;
                        grantPermissionsToService();
                        // 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();
            mHandler.postDelayed(mRestartImsServiceRunnable, mRebindRetry.getRetryTimeout());
        }
    }

    /**
     * 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<>();

    // 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(int, int)} 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();
        }
    }

    // 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()};
        // Uncommented once changes to PackageManager go in (ims_resolver_3_3)
//        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 addImsServiceFeature(Pair<Integer, Integer> featurePair) throws RemoteException {
        if (mIImsServiceController == null || mCallbacks == null) {
            Log.w(LOG_TAG, "addImsServiceFeature called with null values.");
            return;
        }
        mIImsServiceController.createImsFeature(featurePair.first, featurePair.second);
        // 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);
    }

    private void removeImsServiceFeature(Pair<Integer, Integer> featurePair)
            throws RemoteException {
        if (mIImsServiceController == null || mCallbacks == null) {
            Log.w(LOG_TAG, "removeImsServiceFeature called with null values.");
            return;
        }
        mIImsServiceController.removeImsFeature(featurePair.first, featurePair.second);
        // 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;
        }
    }
}
+907 −0

File added.

Preview size limit exceeded, changes collapsed.

+441 −0

File added.

Preview size limit exceeded, changes collapsed.

+63 −0
Original line number Diff line number Diff line
/*
 * 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.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.test.InstrumentationRegistry;

import org.mockito.MockitoAnnotations;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Helper class to load Mockito Resources into a test.
 */
public class ImsTestBase {

    protected Context mContext;

    public void setUp() throws Exception {
        mContext = InstrumentationRegistry.getTargetContext();
        MockitoAnnotations.initMocks(this);
        // Set up the looper if it does not exist on the test thread.
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
    }

    public void tearDown() throws Exception {
    }

    protected final void waitForHandlerAction(Handler h, long timeoutMillis) {
        waitForHandlerActionDelayed(h, timeoutMillis, 0 /*delayMs*/);
    }

    protected final void waitForHandlerActionDelayed(Handler h, long timeoutMillis, long delayMs) {
        final CountDownLatch lock = new CountDownLatch(1);
        h.postDelayed(lock::countDown, delayMs);
        while (lock.getCount() > 0) {
            try {
                lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                // do nothing
            }
        }
    }
}
Loading