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

Commit eb073796 authored by Brad Ebinger's avatar Brad Ebinger
Browse files

Add BinderCacheManager: caches Binder & notifies listeners upon death

Adds a BinderCacheManager class, which does the following:
1) Manages a cached Binder node and recreates it when the Binder node dies.
2) Allows Runnables to be associated with the cached Binder,
which will be called when the node dies.
3) Associates the Runnable with an Object key, which can be used to
remove the runnable without needing to keep a separate map
of Object->Runnable pairs when the user unregisters a persistent
listener.

Test: atest CtsTelephonyTestCases FrameworksTelephonyTests:BinderCacheManagerTest
Change-Id: I23a97371435b7aca8153a30997b91bd324a781a0
parent ec6681c9
Loading
Loading
Loading
Loading
+197 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.telephony;

import android.annotation.NonNull;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Keeps track of the connection to a Binder node, refreshes the cache if the node dies, and lets
 * interested parties register listeners on the node to be notified when the node has died via the
 * registered {@link Runnable}.
 * @param <T> The IInterface representing the Binder type that this manager will be managing the
 *           cache of.
 * @hide
 */
public class BinderCacheManager<T extends IInterface> {

    /**
     * Factory class for creating new IInterfaces in the case that {@link #getBinder()} is
     * called and there is no active binder available.
     * @param <T> The IInterface that should be cached and returned to the caller when
     * {@link #getBinder()} is called until the Binder node dies.
     */
    public interface BinderInterfaceFactory<T> {
        /**
         * @return A new instance of the Binder node, which will be cached until it dies.
         */
        T create();
    }

    /**
     * Tracks the cached Binder node as well as the listeners that were associated with that
     * Binder node during its lifetime. If the Binder node dies, the listeners will be called and
     * then this tracker will be unlinked and cleaned up.
     */
    private class BinderDeathTracker implements IBinder.DeathRecipient {

        private final T mConnection;
        private final HashMap<Object, Runnable> mListeners = new HashMap<>();

        /**
         * Create a tracker to cache the Binder node and add the ability to listen for the cached
         * interface's death.
         */
        BinderDeathTracker(@NonNull T connection) {
            mConnection = connection;
            try {
                mConnection.asBinder().linkToDeath(this, 0 /*flags*/);
            } catch (RemoteException e) {
                // isAlive will return false.
            }
        }

        public boolean addListener(Object key, Runnable r) {
            synchronized (mListeners) {
                if (!isAlive()) return false;
                mListeners.put(key, r);
                return true;
            }
        }

        public void removeListener(Object runnableKey) {
            synchronized (mListeners) {
                mListeners.remove(runnableKey);
            }
        }

        @Override
        public void binderDied() {
            ArrayList<Runnable> listeners;
            synchronized (mListeners) {
                listeners = new ArrayList<>(mListeners.values());
                mListeners.clear();
                try {
                    mConnection.asBinder().unlinkToDeath(this, 0 /*flags*/);
                } catch (NoSuchElementException e) {
                    // No need to worry about this, this means the death recipient was never linked.
                }
            }
            listeners.forEach(Runnable::run);
        }

        /**
         * @return The cached Binder.
         */
        public T getConnection() {
            return mConnection;
        }

        /**
         * @return true if the cached Binder is alive at the time of calling, false otherwise.
         */
        public boolean isAlive() {
            return mConnection.asBinder().isBinderAlive();
        }
    }

    private final BinderInterfaceFactory<T> mBinderInterfaceFactory;
    private final AtomicReference<BinderDeathTracker> mCachedConnection;

    /**
     * Create a new instance, which manages a cached IInterface and creates new ones using the
     * provided factory when the cached IInterface dies.
     * @param factory The factory used to create new Instances of the cached IInterface when it
     *                dies.
     */
    public BinderCacheManager(BinderInterfaceFactory<T> factory) {
        mBinderInterfaceFactory = factory;
        mCachedConnection = new AtomicReference<>();
    }

    /**
     * Get the binder node connection and add a Runnable to be run if this Binder dies. Once this
     * Runnable is run, the Runnable itself is discarded and must be added again.
     * <p>
     * Note: There should be no assumptions here as to which Thread this Runnable is called on. If
     * the Runnable should be called on a specific thread, it should be up to the caller to handle
     * that in the runnable implementation.
     * @param runnableKey The Key associated with this runnable so that it can be removed later
     *                    using {@link #removeRunnable(Object)} if needed.
     * @param deadRunnable The runnable that will be run if the cached Binder node dies.
     * @return T if the runnable was added or {@code null} if the connection is not alive right now
     * and the associated runnable was never added.
     */
    public T listenOnBinder(Object runnableKey, Runnable deadRunnable) {
        if (runnableKey == null || deadRunnable == null) return null;
        BinderDeathTracker tracker = getTracker();
        if (tracker == null) return null;

        boolean addSucceeded = tracker.addListener(runnableKey, deadRunnable);
        return addSucceeded ? tracker.getConnection() : null;
    }

    /**
     * @return The cached Binder node. May return null if the requested Binder node is not currently
     * available.
     */
    public T getBinder() {
        BinderDeathTracker tracker = getTracker();
        return (tracker != null) ? tracker.getConnection() : null;
    }

    /**
     * Removes a previously registered runnable associated with the returned  cached Binder node
     * using the key it was registered with in {@link #listenOnBinder} if the runnable still exists.
     * @param runnableKey The key that was used to register the Runnable earlier.
     * @return The cached Binder node that the runnable used to registered to or null if the cached
     * Binder node is not alive anymore.
     */
    public T removeRunnable(Object runnableKey) {
        if (runnableKey == null) return null;
        BinderDeathTracker tracker = getTracker();
        if (tracker == null) return null;
        tracker.removeListener(runnableKey);
        return tracker.getConnection();
    }

    /**
     * @return The BinderDeathTracker container, which contains the cached IInterface instance or
     * null if it is not available right now.
     */
    private BinderDeathTracker getTracker() {
        return mCachedConnection.updateAndGet((oldVal) -> {
            BinderDeathTracker tracker = oldVal;
            // Update cache if no longer alive. BinderDied will eventually be called on the tracker,
            // which will call listeners & clean up.
            if (tracker == null || !tracker.isAlive()) {
                T binder = mBinderInterfaceFactory.create();
                tracker = (binder != null) ? new BinderDeathTracker(binder) : null;

            }
            return (tracker != null && tracker.isAlive()) ? tracker : null;
        });
    }

}