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

Commit 4c7e9d1a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Reduce LocationManager memory usage" into sc-dev

parents 5fc4b255 204b1338
Loading
Loading
Loading
Loading
+14 −36
Original line number Diff line number Diff line
@@ -16,54 +16,43 @@

package com.android.internal.listeners;


import android.annotation.NonNull;
import android.annotation.Nullable;

import com.android.internal.util.Preconditions;

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

/**
 * A listener registration object which holds data associated with a listener, such the executor
 * the listener should run on.
 * A listener transport object which can run listener operations on an executor.
 *
 * @param <TListener> listener type
 */
public class ListenerTransport<TListener> {

    private final Executor mExecutor;
public interface ListenerTransport<TListener> {

    private volatile @Nullable TListener mListener;

    protected ListenerTransport(@NonNull Executor executor, @NonNull TListener listener) {
        Preconditions.checkArgument(executor != null, "invalid null executor");
        Preconditions.checkArgument(listener != null, "invalid null listener/callback");
        mExecutor = executor;
        mListener = listener;
    }
    /**
     * Should return a valid listener until {@link #unregister()} is invoked, and must return
     * null after that. Recommended (but not required) that this is implemented via a volatile
     * variable.
     */
    @Nullable TListener getListener();

    /**
     * Prevents any listener invocations that happen-after this call.
     * Must be implemented so that {@link #getListener()} returns null after this is invoked.
     */
    public final void unregister() {
        mListener = null;
    }
    void unregister();

    /**
     * Executes the given operation for the listener.
     */
    public final void execute(@NonNull Consumer<TListener> operation) {
    default void execute(Executor executor, Consumer<TListener> operation) {
        Objects.requireNonNull(operation);

        if (mListener == null) {
        if (getListener() == null) {
            return;
        }

        mExecutor.execute(() -> {
            TListener listener = mListener;
        executor.execute(() -> {
            TListener listener = getListener();
            if (listener == null) {
                return;
            }
@@ -71,15 +60,4 @@ public class ListenerTransport<TListener> {
            operation.accept(listener);
        });
    }

    @Override
    public final boolean equals(Object obj) {
        // intentionally bound to reference equality so removal works as expected
        return this == obj;
    }

    @Override
    public final int hashCode() {
        return super.hashCode();
    }
}
+97 −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 com.android.internal.listeners;

import android.os.RemoteException;

import com.android.internal.annotations.GuardedBy;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * A listener transport manager which handles mappings between the client facing listener and system
 * server facing transport. Supports transports which may be removed either from the client side or
 * from the system server side without leaking memory.
 *
 * @param <TTransport>> transport type
 */
public abstract class ListenerTransportManager<TTransport extends ListenerTransport<?>> {

    @GuardedBy("mRegistrations")
    private final Map<Object, WeakReference<TTransport>> mRegistrations;

    protected ListenerTransportManager() {
        // using weakhashmap means that the transport may be GCed if the server drops its reference,
        // and thus the listener may be GCed as well if the client drops that reference. if the
        // server will never drop a reference without warning (ie, transport removal may only be
        // initiated from the client side), then arraymap or similar may be used without fear of
        // memory leaks.
        mRegistrations = new WeakHashMap<>();
    }

    /**
     * Adds a new transport with the given listener key.
     */
    public final void addListener(Object key, TTransport transport) {
        try {
            synchronized (mRegistrations) {
                // ordering of operations is important so that if an error occurs at any point we
                // are left in a reasonable state
                registerTransport(transport);
                WeakReference<TTransport> oldTransportRef = mRegistrations.put(key,
                        new WeakReference<>(transport));
                if (oldTransportRef != null) {
                    TTransport oldTransport = oldTransportRef.get();
                    if (oldTransport != null) {
                        oldTransport.unregister();
                        unregisterTransport(oldTransport);
                    }
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Removes the transport with the given listener key.
     */
    public final void removeListener(Object key) {
        try {
            synchronized (mRegistrations) {
                // ordering of operations is important so that if an error occurs at any point we
                // are left in a reasonable state
                WeakReference<TTransport> transportRef = mRegistrations.remove(key);
                if (transportRef != null) {
                    TTransport transport = transportRef.get();
                    if (transport != null) {
                        transport.unregister();
                        unregisterTransport(transport);
                    }
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    protected abstract void registerTransport(TTransport transport) throws RemoteException;

    protected abstract void unregisterTransport(TTransport transport) throws RemoteException;
}
+0 −258
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 com.android.internal.listeners;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;

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

import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A listener multiplexer designed for use by client-side code. This class ensures that listeners
 * are never invoked while a lock is held. This class is only useful for multiplexing listeners -
 * if all client listeners can be combined into a single server request, and all server results will
 * be delivered to all clients.
 *
 * By default, the multiplexer will replace requests on the server simply by registering the new
 * request and trusting the server to know this is replacing the old request. If the server needs to
 * have the old request unregistered first, subclasses should override
 * {@link #reregisterWithServer(Object, Object)}.
 *
 * @param <TRequest>  listener request type, may be Void
 * @param <TListener> listener type
 */
public abstract class ListenerTransportMultiplexer<TRequest, TListener> {

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> mRegistrations =
            new ArrayMap<>();

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

    @GuardedBy("mLock")
    private TRequest mCurrentRequest;

    /**
     * Should be implemented to register the given merged request with the server.
     *
     * @see #reregisterWithServer(Object, Object)
     */
    protected abstract void registerWithServer(TRequest mergedRequest) throws RemoteException;

    /**
     * Invoked when the server already has a request registered, and it is being replaced with a new
     * request. The default implementation simply registers the new request, trusting the server to
     * overwrite the old request.
     */
    protected void reregisterWithServer(TRequest oldMergedRequest, TRequest mergedRequest)
            throws RemoteException {
        registerWithServer(mergedRequest);
    }

    /**
     * Should be implemented to unregister from the server.
     */
    protected abstract void unregisterWithServer() throws RemoteException;

    /**
     * Called in order to generate a merged request from the given requests. The list of requests
     * will never be empty.
     */
    protected @Nullable TRequest mergeRequests(Collection<TRequest> requests) {
        if (Build.IS_DEBUGGABLE) {
            for (TRequest request : requests) {
                // if using non-null requests then implementations must override this method
                Preconditions.checkState(request == null);
            }
        }

        return null;
    }

    /**
     * Adds a new listener with no request, using the listener as the key.
     */
    public void addListener(@NonNull TListener listener, @NonNull Executor executor) {
        addListener(listener, null, listener, executor);
    }

    /**
     * Adds a new listener with the given request, using the listener as the key.
     */
    public void addListener(@Nullable TRequest request, @NonNull TListener listener,
            @NonNull Executor executor) {
        addListener(listener, request, listener, executor);
    }

    /**
     * Adds a new listener with the given request using a custom key.
     */
    public void addListener(@NonNull Object key, @Nullable TRequest request,
            @NonNull TListener listener, @NonNull Executor executor) {
        Objects.requireNonNull(key);
        RequestListenerTransport<TRequest, TListener> registration =
                new RequestListenerTransport<>(request, executor, listener);

        synchronized (mLock) {
            ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> newRegistrations =
                    new ArrayMap<>(mRegistrations.size() + 1);
            newRegistrations.putAll(mRegistrations);
            RequestListenerTransport<TRequest, TListener> old = newRegistrations.put(key,
                    registration);
            mRegistrations = newRegistrations;

            if (old != null) {
                old.unregister();
            }

            updateService();
        }
    }

    /**
     * Removes the listener with the given key.
     */
    public void removeListener(@NonNull Object key) {
        Objects.requireNonNull(key);

        synchronized (mLock) {
            if (!mRegistrations.containsKey(key)) {
                return;
            }

            ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> newRegistrations =
                    new ArrayMap<>(mRegistrations);
            RequestListenerTransport<TRequest, TListener> old = newRegistrations.remove(key);
            mRegistrations = newRegistrations;

            if (old != null) {
                old.unregister();
                updateService();
            }
        }
    }

    private void updateService() {
        synchronized (mLock) {
            if (mRegistrations.isEmpty()) {
                mCurrentRequest = null;
                if (mServiceRegistered) {
                    try {
                        mServiceRegistered = false;
                        unregisterWithServer();
                    } catch (RemoteException e) {
                        throw e.rethrowFromSystemServer();
                    }
                }
                return;
            }

            ArrayList<TRequest> requests = new ArrayList<>(mRegistrations.size());
            for (int i = 0; i < mRegistrations.size(); i++) {
                requests.add(mRegistrations.valueAt(i).getRequest());
            }

            TRequest merged = mergeRequests(requests);
            if (!mServiceRegistered || !Objects.equals(merged, mCurrentRequest)) {
                TRequest old = mCurrentRequest;
                mCurrentRequest = null;
                try {
                    if (mServiceRegistered) {
                        // if a remote exception is thrown the service should not be registered
                        mServiceRegistered = false;
                        reregisterWithServer(old, merged);
                    } else {
                        registerWithServer(merged);
                    }
                    mCurrentRequest = merged;
                    mServiceRegistered = true;
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
        }
    }

    protected final void deliverToListeners(Consumer<TListener> operation) {
        ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> registrations;
        synchronized (mLock) {
            registrations = mRegistrations;
        }

        try {
            for (int i = 0; i < registrations.size(); i++) {
                registrations.valueAt(i).execute(operation);
            }
        } finally {
            onOperationFinished(operation);
        }
    }

    /**
     * Invoked when an operation is finished. This method will always be called once for every call
     * to {@link #deliverToListeners(Consumer)}, regardless of whether the operation encountered any
     * error or failed to execute in any way for any listeners.
     */
    protected void onOperationFinished(@NonNull Consumer<TListener> operation) {}

    /**
     * Dumps debug information.
     */
    public void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
        ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> registrations;
        synchronized (mLock) {
            registrations = mRegistrations;

            ipw.print("service: ");
            if (mServiceRegistered) {
                if (mCurrentRequest == null) {
                    ipw.print("request registered");
                } else {
                    ipw.print("request registered - " + mCurrentRequest);
                }
            } else {
                ipw.print("unregistered");
            }
            ipw.println();
        }

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

            ipw.increaseIndent();
            for (int i = 0; i < registrations.size(); i++) {
                ipw.print(registrations.valueAt(i));
            }
            ipw.decreaseIndent();
        }
    }
}
+0 −45
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 com.android.internal.listeners;

import android.annotation.Nullable;

import java.util.concurrent.Executor;

/**
 * A listener transport with an associated request.
 *
 * @param <TRequest>  request type
 * @param <TListener> listener type
 */
public class RequestListenerTransport<TRequest, TListener> extends ListenerTransport<TListener> {

    private final @Nullable TRequest mRequest;

    protected RequestListenerTransport(@Nullable TRequest request, Executor executor,
            TListener listener) {
        super(executor, listener);
        mRequest = request;
    }

    /**
     * Returns the request associated with this transport.
     */
    public final @Nullable TRequest getRequest() {
        return mRequest;
    }
}
+0 −194
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 com.android.internal.listeners;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.os.Handler;
import android.os.Looper;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import junit.framework.TestCase;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Collection;
import java.util.Comparator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class ListenerTransportMultiplexerTest extends TestCase {

    TestMultiplexer mMultiplexer;

    @Before
    public void setUp() {
        mMultiplexer = new TestMultiplexer();
    }

    @Test
    public void testAdd() {
        Runnable runnable = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable, Runnable::run);
        assertThat(mMultiplexer.mRegistered).isTrue();
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);

        mMultiplexer.notifyListeners();
        verify(runnable, times(1)).run();
    }

    @Test
    public void testAdd_Multiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(0, runnable2, Runnable::run);

        mMultiplexer.notifyListeners();
        verify(runnable1).run();
        verify(runnable2).run();
    }

    @Test
    public void testRemove() {
        Runnable runnable = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable, Runnable::run);
        mMultiplexer.removeListener(runnable);
        assertThat(mMultiplexer.mRegistered).isFalse();

        mMultiplexer.notifyListeners();
        verify(runnable, never()).run();
    }

    @Test
    public void testRemove_Multiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(1, runnable2, Runnable::run);
        mMultiplexer.removeListener(runnable1);

        mMultiplexer.notifyListeners();
        verify(runnable1, never()).run();
        verify(runnable2).run();
    }

    @Test
    public void testMergeMultiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);
        Runnable runnable3 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(1, runnable2, Runnable::run);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(1)).run();
        verify(runnable2, times(1)).run();
        verify(runnable3, times(0)).run();

        mMultiplexer.addListener(0, runnable3, Runnable::run);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(2)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(1)).run();

        mMultiplexer.removeListener(runnable2);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(3)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(2)).run();

        mMultiplexer.removeListener(runnable1);
        mMultiplexer.removeListener(runnable3);
        mMultiplexer.notifyListeners();
        verify(runnable1, times(3)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(2)).run();
    }

    @Test(timeout = 5000)
    public void testReentrancy() {
        AtomicReference<Runnable> runnable = new AtomicReference<>();
        runnable.set(() -> mMultiplexer.removeListener(runnable.get()));

        mMultiplexer.addListener(0, runnable.get(), command -> {
            CountDownLatch latch = new CountDownLatch(1);
            new Handler(Looper.getMainLooper()).post(() -> {
                command.run();
                latch.countDown();
            });
            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new AssertionError(e);
            }
        });

        mMultiplexer.notifyListeners();
        assertThat(mMultiplexer.mRegistered).isFalse();
    }

    private static class TestMultiplexer extends ListenerTransportMultiplexer<Integer, Runnable> {

        boolean mRegistered;
        int mMergedRequest;

        TestMultiplexer() {
        }

        public void notifyListeners() {
            deliverToListeners(Runnable::run);
        }

        @Override
        protected void registerWithServer(Integer mergedRequest) {
            mRegistered = true;
            mMergedRequest = mergedRequest;
        }

        @Override
        protected void unregisterWithServer() {
            mRegistered = false;
        }

        @Override
        protected Integer mergeRequests(Collection<Integer> requests) {
            return requests.stream().max(Comparator.naturalOrder()).get();
        }
    }
}
Loading