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

Commit 6257ba86 authored by Soonil Nagarkar's avatar Soonil Nagarkar
Browse files

Refactor GNSS listeners

Gnss listeners currently simply ignore requests that are not made in the
foreground or with appropriate permissions, even though these are
transitory attributes. In addition, some gnss listener use appops APIs
incorrectly. The RemoteListenerHelper pattern is not great either, and
makes it extremely difficult to write clear code, as well as duplicates
much of the functionality provided by AbstractListenerManager on the
client side. This CL unifies logic for multiplexing listeners so it can
be used across both the client and server side, and updates all GNSS
listeners to use the new model, which properly respects foreground,
appops, permissions, gnss enablement, and capabilities.

Test: presubmits + manual
Change-Id: I8f158e860e10c3ed43b246f6f3687169ac134343
parent 9880ac98
Loading
Loading
Loading
Loading
+0 −220
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2014 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.location;

import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.RemoteException;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A base class to manage listeners that have a 1:N -> source:listener relationship.
 *
 * @hide
 */
abstract class AbstractListenerManager<TRequest, TListener> {

    private static class Registration<TRequest, TListener> {
        private final Executor mExecutor;
        @Nullable private TRequest mRequest;
        @Nullable private volatile TListener mListener;

        private Registration(@Nullable TRequest request, Executor executor, TListener listener) {
            Preconditions.checkArgument(listener != null, "invalid null listener/callback");
            Preconditions.checkArgument(executor != null, "invalid null executor");
            mExecutor = executor;
            mListener = listener;
            mRequest = request;
        }

        @Nullable
        public TRequest getRequest() {
            return mRequest;
        }

        private void unregister() {
            mRequest = null;
            mListener = null;
        }

        private void execute(Consumer<TListener> operation) {
            mExecutor.execute(
                    obtainRunnable(Registration<TRequest, TListener>::accept, this, operation)
                            .recycleOnUse());
        }

        private void accept(Consumer<TListener> operation) {
            TListener listener = mListener;
            if (listener == null) {
                return;
            }

            // we may be under the binder identity if a direct executor is used
            long identity = Binder.clearCallingIdentity();
            try {
                operation.accept(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    }

    @GuardedBy("mListeners")
    private final ArrayMap<Object, Registration<TRequest, TListener>> mListeners =
            new ArrayMap<>();

    @GuardedBy("mListeners")
    @Nullable
    private TRequest mMergedRequest;

    public boolean addListener(@NonNull TListener listener, @NonNull Handler handler)
            throws RemoteException {
        return addInternal(/* request= */ null, listener, handler);
    }

    public boolean addListener(@NonNull TListener listener, @NonNull Executor executor)
            throws RemoteException {
        return addInternal(/* request= */ null, listener, executor);
    }

    public boolean addListener(@Nullable TRequest request, @NonNull TListener listener,
            @NonNull Handler handler) throws RemoteException {
        return addInternal(request, listener, handler);
    }

    public boolean addListener(@Nullable TRequest request, @NonNull TListener listener,
            @NonNull Executor executor) throws RemoteException {
        return addInternal(request, listener, executor);
    }

    protected final boolean addInternal(@Nullable TRequest request, @NonNull Object listener,
            @NonNull Handler handler) throws RemoteException {
        return addInternal(request, listener, new HandlerExecutor(handler));
    }

    protected final boolean addInternal(@Nullable TRequest request, @NonNull Object listener,
            @NonNull Executor executor)
            throws RemoteException {
        Preconditions.checkArgument(listener != null, "invalid null listener/callback");
        return addInternal(listener, new Registration<>(request, executor, convertKey(listener)));
    }

    private boolean addInternal(Object key, Registration<TRequest, TListener> registration)
            throws RemoteException {
        Preconditions.checkNotNull(registration);

        synchronized (mListeners) {
            boolean initialRequest = mListeners.isEmpty();

            Registration<TRequest, TListener> oldRegistration = mListeners.put(key, registration);
            if (oldRegistration != null) {
                oldRegistration.unregister();
            }
            TRequest merged = mergeRequests();

            if (initialRequest || !Objects.equals(merged, mMergedRequest)) {
                mMergedRequest = merged;
                if (!initialRequest) {
                    unregisterService();
                }
                registerService(mMergedRequest);
            }

            return true;
        }
    }

    public void removeListener(Object listener) throws RemoteException {
        synchronized (mListeners) {
            Registration<TRequest, TListener> oldRegistration = mListeners.remove(listener);
            if (oldRegistration == null) {
                return;
            }
            oldRegistration.unregister();

            boolean lastRequest = mListeners.isEmpty();
            TRequest merged = lastRequest ? null : mergeRequests();
            boolean newRequest = !lastRequest && !Objects.equals(merged, mMergedRequest);

            if (lastRequest || newRequest) {
                unregisterService();
                mMergedRequest = merged;
                if (newRequest) {
                    registerService(mMergedRequest);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected TListener convertKey(@NonNull Object listener) {
        return (TListener) listener;
    }

    protected abstract boolean registerService(TRequest request) throws RemoteException;
    protected abstract void unregisterService() throws RemoteException;

    @Nullable
    protected TRequest merge(@NonNull TRequest[] requests) {
        for (TRequest request : requests) {
            Preconditions.checkArgument(request == null,
                    "merge() has to be overridden for non-null requests.");
        }
        return null;
    }

    protected void execute(Consumer<TListener> operation) {
        synchronized (mListeners) {
            for (Registration<TRequest, TListener> registration : mListeners.values()) {
                registration.execute(operation);
            }
        }
    }

    @GuardedBy("mListeners")
    @SuppressWarnings("unchecked")
    @Nullable
    private TRequest mergeRequests() {
        Preconditions.checkState(Thread.holdsLock(mListeners));

        if (mListeners.isEmpty()) {
            return null;
        }

        if (mListeners.size() == 1) {
            return mListeners.valueAt(0).getRequest();
        }

        TRequest[] requests = (TRequest[]) new Object[mListeners.size()];
        for (int index = 0; index < mListeners.size(); index++) {
            requests[index] = mListeners.valueAt(index).getRequest();
        }
        return merge(requests);
    }
}
+3 −1
Original line number Original line Diff line number Diff line
@@ -70,7 +70,9 @@ public final class GnssRequest implements Parcelable {
    public String toString() {
    public String toString() {
        StringBuilder s = new StringBuilder();
        StringBuilder s = new StringBuilder();
        s.append("GnssRequest[");
        s.append("GnssRequest[");
        s.append("FullTracking=").append(mFullTracking);
        if (mFullTracking) {
            s.append("FullTracking");
        }
        s.append(']');
        s.append(']');
        return s.toString();
        return s.toString();
    }
    }
+13 −18
Original line number Original line Diff line number Diff line
@@ -57,10 +57,6 @@ interface ILocationManager
            in PendingIntent intent, String packageName, String featureId);
            in PendingIntent intent, String packageName, String featureId);
    void removeGeofence(in Geofence fence, in PendingIntent intent, String packageName);
    void removeGeofence(in Geofence fence, in PendingIntent intent, String packageName);


    boolean registerGnssStatusCallback(IGnssStatusListener callback, String packageName,
            String featureId);
    void unregisterGnssStatusCallback(IGnssStatusListener callback);

    boolean geocoderIsPresent();
    boolean geocoderIsPresent();
    String getFromLocation(double latitude, double longitude, int maxResults,
    String getFromLocation(double latitude, double longitude, int maxResults,
        in GeocoderParams params, out List<Address> addrs);
        in GeocoderParams params, out List<Address> addrs);
@@ -69,31 +65,30 @@ interface ILocationManager
        double upperRightLatitude, double upperRightLongitude, int maxResults,
        double upperRightLatitude, double upperRightLongitude, int maxResults,
        in GeocoderParams params, out List<Address> addrs);
        in GeocoderParams params, out List<Address> addrs);


    boolean addGnssMeasurementsListener(in GnssRequest request,
            in IGnssMeasurementsListener listener,
            String packageName, String featureId);
    void injectGnssMeasurementCorrections(in GnssMeasurementCorrections corrections,
            in String packageName);
    long getGnssCapabilities();
    long getGnssCapabilities();
    int getGnssYearOfHardware();
    String getGnssHardwareModelName();

    void registerGnssStatusCallback(in IGnssStatusListener callback, String packageName, String featureId);
    void unregisterGnssStatusCallback(in IGnssStatusListener callback);

    void addGnssMeasurementsListener(in GnssRequest request, in IGnssMeasurementsListener listener, String packageName, String featureId);
    void removeGnssMeasurementsListener(in IGnssMeasurementsListener listener);
    void removeGnssMeasurementsListener(in IGnssMeasurementsListener listener);


    boolean addGnssAntennaInfoListener(in IGnssAntennaInfoListener listener,
    void addGnssAntennaInfoListener(in IGnssAntennaInfoListener listener, String packageName, String featureId);
             String packageName, String featureId);
    void removeGnssAntennaInfoListener(in IGnssAntennaInfoListener listener);
    void removeGnssAntennaInfoListener(in IGnssAntennaInfoListener listener);


    boolean addGnssNavigationMessageListener(in IGnssNavigationMessageListener listener,
    void addGnssNavigationMessageListener(in IGnssNavigationMessageListener listener, String packageName, String featureId);
             String packageName, String featureId);
    void removeGnssNavigationMessageListener(in IGnssNavigationMessageListener listener);
    void removeGnssNavigationMessageListener(in IGnssNavigationMessageListener listener);


    int getGnssYearOfHardware();
    void injectGnssMeasurementCorrections(in GnssMeasurementCorrections corrections, String packageName);
    String getGnssHardwareModelName();


    int getGnssBatchSize(String packageName);
    int getGnssBatchSize(String packageName);
    boolean addGnssBatchingCallback(in IBatchedLocationCallback callback, String packageName, String featureId);
    void addGnssBatchingCallback(in IBatchedLocationCallback callback, String packageName, String featureId);
    void removeGnssBatchingCallback();
    void removeGnssBatchingCallback();
    boolean startGnssBatch(long periodNanos, boolean wakeOnFifoFull, String packageName, String featureId);
    void startGnssBatch(long periodNanos, boolean wakeOnFifoFull, String packageName, String featureId);
    void flushGnssBatch(String packageName);
    void flushGnssBatch(String packageName);
    boolean stopGnssBatch();
    void stopGnssBatch();
    void injectLocation(in Location location);
    void injectLocation(in Location location);


    List<String> getAllProviders();
    List<String> getAllProviders();
+181 −219

File changed.

Preview size limit exceeded, changes collapsed.

+411 −0
Original line number Original line 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.location.util.listeners;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Build;
import android.util.ArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * A base class to manage listeners multiplexed from some source.
 *
 * @param <TKey>           key type
 * @param <TRequest>       request type
 * @param <TListener>      listener type
 * @param <TRegistration>  registration type
 * @param <TMergedRequest> merged request type
 * @hide
 */
public abstract class AbstractListenerManager<TKey, TRequest, TListener,
        TRegistration extends AbstractListenerManager.Registration<TRequest, TListener>,
        TMergedRequest> {

    /**
     * A listener registration object which holds data associated with the listener.
     *
     * @param <TRequest>  request type
     * @param <TListener> listener type
     */
    public static class Registration<TRequest, TListener> {

        private final Executor mExecutor;
        private volatile @Nullable TRequest mRequest;
        private volatile @Nullable TListener mListener;

        protected Registration(@Nullable TRequest request, @NonNull Executor executor,
                @NonNull TListener listener) {
            Preconditions.checkArgument(executor != null, "invalid null executor");
            Preconditions.checkArgument(listener != null, "invalid null listener/callback");
            mExecutor = executor;
            mRequest = request;
            mListener = listener;
        }

        /**
         * Returns the request associated with this listener, or null if one wasn't supplied.
         */
        public @Nullable TRequest getRequest() {
            return mRequest;
        }

        /**
         * Returns the listener, or null if this registration is no longer registered.
         */
        protected @Nullable TListener getListener() {
            return mListener;
        }

        boolean register() {
            Preconditions.checkState(mListener != null);
            return onRegister();
        }

        protected final void unregister() {
            if (mListener != null) {
                onUnregister();
                mRequest = null;
                mListener = null;
            }
        }

        /**
         * May be overridden by subclasses. Invoked when registration is occurring. If this returns
         * true, then registration will complete successfully. If this returns false, registration
         * will fail.
         */
        protected boolean onRegister() {
            return true;
        }

        /**
         * May be overridden by subclasses. Invoked before unregistration occurs.
         */
        protected void onUnregister() {}

        void execute(Consumer<TListener> operation) {
            mExecutor.execute(() -> {
                TListener listener = mListener;
                if (listener == null) {
                    return;
                }

                // we may be under the binder identity if a direct executor is used
                long identity = Binder.clearCallingIdentity();
                try {
                    operation.accept(listener);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            });
        }

        @Override
        public String toString() {
            if (mRequest == null) {
                return "[]";
            } else {
                return mRequest.toString();
            }
        }
    }

    @GuardedBy("mRegistrations")
    private final ArrayMap<TKey, TRegistration> mRegistrations = new ArrayMap<>();

    @GuardedBy("mRegistrations")
    private final ReentrancyGuard mReentrancyGuard = new ReentrancyGuard();

    @GuardedBy("mRegistrations")
    private boolean mServiceRegistered = false;

    @GuardedBy("mRegistrations")
    private TMergedRequest mCurrentRequest;

    /**
     * Adds a new registration with the given key. If the registration succeeds,
     * {@link #onRegistrationAdded(Object, Registration)} will be invoked.
     */
    protected final void addRegistration(@NonNull TKey key, @NonNull TRegistration registration) {
        synchronized (mRegistrations) {
            // this class does not support adding listeners reentrantly
            Preconditions.checkState(!mReentrancyGuard.isReentrant());

            if (!registration.register()) {
                registration.unregister();
                return;
            }

            TRegistration old = mRegistrations.put(Objects.requireNonNull(key), registration);
            if (old != null) {
                onRegistrationRemoved(key, old);
                old.unregister();
            }

            onRegistrationAdded(key, registration);
            updateService();
        }
    }

    /**
     * Removes the given registration with the given key. If unregistration occurs,
     * {@link #onRegistrationRemoved(Object, Registration)} will be called.
     */
    protected final void removeRegistration(@NonNull TKey key,
            @NonNull TRegistration registration) {
        synchronized (mRegistrations) {
            if (mRegistrations.remove(key, registration)) {
                unregisterRegistration(key, registration);
            }
        }
    }

    /**
     * Removes the registration with the given key. If unregistration occurs,
     * {@link #onRegistrationRemoved(Object, Registration)} will be called.
     */
    protected final void removeRegistration(@NonNull TKey key) {
        synchronized (mRegistrations) {
            TRegistration registration = mRegistrations.remove(key);
            if (registration != null) {
                unregisterRegistration(key, registration);
            }
        }
    }

    @GuardedBy("mRegistrations")
    private void unregisterRegistration(TKey key, TRegistration registration) {
        if (Build.IS_DEBUGGABLE) {
            Preconditions.checkState(Thread.holdsLock(mRegistrations));
        }

        // this class does not support removing listeners reentrantly
        Preconditions.checkState(!mReentrancyGuard.isReentrant());

        onRegistrationRemoved(key, registration);
        registration.unregister();
        updateService();
    }

    @GuardedBy("mRegistrations")
    private void updateService() {
        if (Build.IS_DEBUGGABLE) {
            Preconditions.checkState(Thread.holdsLock(mRegistrations));
        }

        ArrayList<TRegistration> actives = new ArrayList<>(mRegistrations.size());
        for (int i = 0; i < mRegistrations.size(); i++) {
            TRegistration registration = mRegistrations.valueAt(i);
            if (isActive(registration)) {
                actives.add(registration);
            }
        }

        if (actives.isEmpty()) {
            if (mServiceRegistered) {
                unregisterService();
                mServiceRegistered = false;
            }
            mCurrentRequest = null;
            return;
        }

        TMergedRequest merged = mergeRequests(actives);
        if (!mServiceRegistered || !Objects.equals(merged, mCurrentRequest)) {
            if (mServiceRegistered) {
                unregisterService();
            }
            mCurrentRequest = merged;
            mServiceRegistered = registerService(mCurrentRequest);
        }
    }

    /**
     * Defines whether a registration is currently active or not. Only active registrations will be
     * considered within {@link #mergeRequests(List)} to calculate the merged request, and listener
     * invocations will only be delivered to active requests. If a registration's active state
     * changes, {@link #updateRegistrations(Function)} should be invoked with a function that
     * returns true for any registrations that have changed their active state in order to inform
     * this manager of the active status change.
     */
    protected boolean isActive(@NonNull TRegistration registration) {
        return true;
    }

    /**
     * Performs some function on all (not just active) registrations. The function should return
     * true if the active state of the registration has changed, or if the change to the
     * registration may have changed the result of {@link #mergeRequests(List)}.
     */
    protected final void updateRegistrations(@NonNull Function<TRegistration, Boolean> function) {
        synchronized (mRegistrations) {
            boolean changed = false;
            try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                for (int i = 0; i < mRegistrations.size(); i++) {
                    changed |= function.apply(mRegistrations.valueAt(i));
                }
            }
            if (changed) {
                updateService();
            }
        }
    }

    /**
     * Called in order to generate a merged request from all active requests. The list of
     * registrations will never be empty.
     */
    @Nullable
    protected TMergedRequest mergeRequests(@NonNull List<TRegistration> registrations) {
        if (Build.IS_DEBUGGABLE) {
            for (TRegistration registration : registrations) {
                // if using non-null requests then implementations must override this method
                Preconditions.checkState(registration.getRequest() == null);
            }
        }

        return null;
    }

    /**
     * Should be implemented to register the service with the given request, and should return true
     * if registration succeeds.
     */
    protected abstract boolean registerService(@Nullable TMergedRequest mergedRequest);

    /**
     * Should be implemented to unregister the service.
     */
    protected abstract void unregisterService();

    /**
     * Invoked when a registration is added.
     */
    protected void onRegistrationAdded(@NonNull TKey key, @NonNull TRegistration registration) {}

    /**
     * Invoked when a registration is removed.
     */
    protected void onRegistrationRemoved(@NonNull TKey key, @NonNull TRegistration registration) {}

    /**
     * Executes the given delivery operation for all active listeners.
     */
    protected final void deliverToListeners(@NonNull Consumer<TListener> operation) {
        deliverToListeners(operation, registration -> true);
    }

    /**
     * Executes the given delivery operation for all active listeners which pass the given
     * predicate.
     */
    protected final void deliverToListeners(@NonNull Consumer<TListener> operation,
            @NonNull Predicate<TRegistration> deliveryPredicate) {
        synchronized (mRegistrations) {
            try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                for (int i = 0; i < mRegistrations.size(); i++) {
                    TRegistration registration = mRegistrations.valueAt(i);
                    if (isActive(registration) && deliveryPredicate.test(registration)) {
                        registration.execute(operation);
                    }
                }
            }
        }
    }

    /**
     * Dumps debug information.
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");

        synchronized (mRegistrations) {
            ipw.print("service: ");
            ipw.println(serviceStateToString());

            if (!mRegistrations.isEmpty()) {
                ipw.println("listeners:");

                ipw.increaseIndent();
                for (int i = 0; i < mRegistrations.size(); i++) {
                    TRegistration registration = mRegistrations.valueAt(i);
                    ipw.print(registration);
                    if (!isActive(registration)) {
                        ipw.println(" (inactive)");
                    } else {
                        ipw.println();
                    }
                }
                ipw.decreaseIndent();
            }
        }
    }

    /**
     * May be override to provide additional details on service state when dumping the manager
     * state.
     */
    protected String serviceStateToString() {
        if (mServiceRegistered) {
            if (mCurrentRequest == null) {
                return "registered";
            } else {
                return "registered with " + mCurrentRequest;
            }
        } else {
            return "unregistered";
        }
    }

    // this class does not have an idempotent close(), always use via try-with-resources
    private static class ReentrancyGuard implements AutoCloseable {

        private int mGuard = 0;

        ReentrancyGuard() {
        }

        public boolean isReentrant() {
            return mGuard != 0;
        }

        public ReentrancyGuard acquire() {
            ++mGuard;
            return this;
        }

        @Override
        public void close() {
            --mGuard;
        }
    }
}
Loading