Move eventloop to mainline

Test: unit test
Change-Id: Id3a8c9fbd22990c88140f9aa5401b111df559fd2
Bug: 202335820
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java b/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
new file mode 100644
index 0000000..44c9422
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/Annotations.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 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.server.nearby.common.eventloop;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.BinderThread;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * A collection of threading annotations relating to EventLoop. These should be used in conjunction
+ * with {@link UiThread}, {@link BinderThread}, {@link WorkerThread}, and {@link AnyThread}.
+ */
+public class Annotations {
+
+    /**
+     * Denotes that the annotated method or constructor should only be called on the EventLoop
+     * thread.
+     */
+    @Retention(CLASS)
+    @Target({METHOD, CONSTRUCTOR, TYPE})
+    public @interface EventThread {
+    }
+
+    /** Denotes that the annotated method or constructor should only be called on a Network
+     * thread. */
+    @Retention(CLASS)
+    @Target({METHOD, CONSTRUCTOR, TYPE})
+    public @interface NetworkThread {
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java b/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
new file mode 100644
index 0000000..c89366f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/EventLoop.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2021 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.server.nearby.common.eventloop;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Handles executing runnables on a background thread.
+ *
+ * <p>Nearby services follow an event loop model where events can be queued and delivered in the
+ * future. All code that is run in this EventLoop is guaranteed to be run on this thread. The main
+ * advantage of this model is that all modules don't have to deal with synchronization and race
+ * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
+ * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
+ * starting BLE scans, doing a server request and waiting for the response etc.).
+ *
+ * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
+ * deliver a new message to the event queue when the reply of the event happens.
+ */
+// TODO(b/177675274): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+public class EventLoop {
+
+    private final Interface mImpl;
+
+    private EventLoop(Interface impl) {
+        this.mImpl = impl;
+    }
+
+    protected EventLoop(String name) {
+        this(new HandlerEventLoopImpl(name));
+    }
+
+    /** Creates an EventLoop. */
+    public static EventLoop newInstance(String name) {
+        return new EventLoop(name);
+    }
+
+    /** Creates an EventLoop. */
+    public static EventLoop newInstance(String name, Looper looper) {
+        return new EventLoop(new HandlerEventLoopImpl(name, looper));
+    }
+
+    /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
+    public void destroy() {
+        mImpl.destroy();
+    }
+
+    /**
+     * Posts a runnable to this event loop, blocking until the runnable has been executed. This
+     * should
+     * be used rarely. It could be useful, for example, for a runnable that initializes the system
+     * and
+     * must block the posting of all other runnables.
+     *
+     * @param runnable a Runnable to post. This method will not return until the run() method of the
+     *                 given runnable has executed on the background thread.
+     */
+    public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
+        mImpl.postAndWait(runnable);
+    }
+
+    /**
+     * Posts a runnable to this to the front of the event loop, blocking until the runnable has been
+     * executed. This should be used rarely, as it can starve the event loop.
+     *
+     * @param runnable a Runnable to post. This method will not return until the run() method of the
+     *                 given runnable has executed on the background thread.
+     */
+    public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
+        mImpl.postToFrontAndWait(runnable);
+    }
+
+    /** Checks if there are any pending posts of the Runnable in the queue. */
+    public boolean isPosted(NamedRunnable runnable) {
+        return mImpl.isPosted(runnable);
+    }
+
+    /**
+     * Run code on the event loop thread.
+     *
+     * @param runnable the runnable to execute.
+     */
+    public void postRunnable(NamedRunnable runnable) {
+        mImpl.postRunnable(runnable);
+    }
+
+    /**
+     * Run code to be executed when there is no runnable scheduled.
+     *
+     * @param runnable last runnable to execute.
+     */
+    public void postEmptyQueueRunnable(final NamedRunnable runnable) {
+        mImpl.postEmptyQueueRunnable(runnable);
+    }
+
+    /**
+     * Run code on the event loop thread after delayedMillis.
+     *
+     * @param runnable      the runnable to execute.
+     * @param delayedMillis the number of milliseconds before executing the runnable.
+     */
+    public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
+        mImpl.postRunnableDelayed(runnable, delayedMillis);
+    }
+
+    /**
+     * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
+     * with null does nothing.
+     */
+    public void removeRunnable(@Nullable NamedRunnable runnable) {
+        mImpl.removeRunnable(runnable);
+    }
+
+    /** Asserts that the current operation is being executed in the Event Loop's thread. */
+    public void checkThread() {
+        mImpl.checkThread();
+    }
+
+    public Handler getHandler() {
+        return mImpl.getHandler();
+    }
+
+    interface Interface {
+        void destroy();
+
+        void postAndWait(NamedRunnable runnable) throws InterruptedException;
+
+        void postToFrontAndWait(NamedRunnable runnable) throws InterruptedException;
+
+        boolean isPosted(NamedRunnable runnable);
+
+        void postRunnable(NamedRunnable runnable);
+
+        void postEmptyQueueRunnable(NamedRunnable runnable);
+
+        void postRunnableDelayed(NamedRunnable runnable, long delayedMillis);
+
+        void removeRunnable(NamedRunnable runnable);
+
+        void checkThread();
+
+        Handler getHandler();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java b/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
new file mode 100644
index 0000000..018dcdb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/HandlerEventLoopImpl.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2021 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.server.nearby.common.eventloop;
+
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Handles executing runnables on a background thread.
+ *
+ * <p>Nearby services follow an event loop model where events can be queued and delivered in the
+ * future. All code that is run in this package is guaranteed to be run on this thread. The main
+ * advantage of this model is that all modules don't have to deal with synchronization and race
+ * conditions, while making it easy to handle the several asynchronous tasks that are expected to be
+ * needed for this type of provider (such as starting a WiFi scan and waiting for the result,
+ * starting BLE scans, doing a server request and waiting for the response etc.).
+ *
+ * <p>Code that needs to wait for an event should not spawn a new thread nor sleep. It should simply
+ * deliver a new message to the event queue when the reply of the event happens.
+ *
+ * <p>
+ */
+// TODO(b/203471261) use executor instead of handler
+// TODO(b/177675274): Resolve nullness suppression.
+@SuppressWarnings("nullness")
+final class HandlerEventLoopImpl implements EventLoop.Interface {
+    /** The {@link Message#what} code for all messages that we post to the EventLoop. */
+    private static final int WHAT = 0;
+
+    private static final long ELAPSED_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(5);
+    private static final long RUNNABLE_DELAY_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(2);
+    private static final String TAG = HandlerEventLoopImpl.class.getSimpleName();
+    private final MyHandler mHandler;
+
+    private volatile boolean mIsDestroyed = false;
+
+    /** Constructs an EventLoop. */
+    HandlerEventLoopImpl(String name) {
+        this(name, createHandlerThread(name));
+    }
+
+    HandlerEventLoopImpl(String name, Looper looper) {
+
+        mHandler = new MyHandler(looper);
+        Log.d(TAG,
+                "Created EventLoop for thread '" + looper.getThread().getName()
+                        + "(id: " + looper.getThread().getId() + ")'");
+    }
+
+    private static Looper createHandlerThread(String name) {
+        HandlerThread handlerThread = new HandlerThread(name, Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+
+        return handlerThread.getLooper();
+    }
+
+    /**
+     * Wrapper to satisfy Android Lint. {@link Looper#getQueue()} is public and available since ICS,
+     * but was marked @hide until Marshmallow. Tested that this code doesn't crash pre-Marshmallow.
+     * /aosp-ics/frameworks/base/core/java/android/os/Looper.java?l=218
+     */
+    @SuppressLint("NewApi")
+    private static MessageQueue getQueue(Handler handler) {
+        return handler.getLooper().getQueue();
+    }
+
+    /** Marks the EventLoop as destroyed. Any further messages received will be ignored. */
+    @Override
+    public void destroy() {
+        Looper looper = mHandler.getLooper();
+        Log.d(TAG,
+                "Destroying EventLoop for thread " + looper.getThread().getName()
+                        + " (id: " + looper.getThread().getId() + ")");
+        looper.quit();
+        mIsDestroyed = true;
+    }
+
+    /**
+     * Posts a runnable to this event loop, blocking until the runnable has been executed. This
+     * should
+     * be used rarely. It could be useful, for example, for a runnable that initializes the system
+     * and
+     * must block the posting of all other runnables.
+     *
+     * @param runnable a Runnable to post. This method will not return until the run() method of the
+     *                 given runnable has executed on the background thread.
+     */
+    @Override
+    public void postAndWait(final NamedRunnable runnable) throws InterruptedException {
+        internalPostAndWait(runnable, false);
+    }
+
+    @Override
+    public void postToFrontAndWait(final NamedRunnable runnable) throws InterruptedException {
+        internalPostAndWait(runnable, true);
+    }
+
+    /** Checks if there are any pending posts of the Runnable in the queue. */
+    @Override
+    public boolean isPosted(NamedRunnable runnable) {
+        return mHandler.hasMessages(WHAT, runnable);
+    }
+
+    /**
+     * Run code on the event loop thread.
+     *
+     * @param runnable the runnable to execute.
+     */
+    @Override
+    public void postRunnable(NamedRunnable runnable) {
+        Log.d(TAG, "Posting " + runnable);
+        mHandler.post(runnable, 0L, false);
+    }
+
+    /**
+     * Run code to be executed when there is no runnable scheduled.
+     *
+     * @param runnable last runnable to execute.
+     */
+    @Override
+    public void postEmptyQueueRunnable(final NamedRunnable runnable) {
+        mHandler.post(
+                () ->
+                        getQueue(mHandler)
+                                .addIdleHandler(
+                                        () -> {
+                                            if (mHandler.hasMessages(WHAT)) {
+                                                return true;
+                                            } else {
+                                                // Only stop if start has not been called since
+                                                // this was queued
+                                                runnable.run();
+                                                return false;
+                                            }
+                                        }));
+    }
+
+    /**
+     * Run code on the event loop thread after delayedMillis.
+     *
+     * @param runnable      the runnable to execute.
+     * @param delayedMillis the number of milliseconds before executing the runnable.
+     */
+    @Override
+    public void postRunnableDelayed(NamedRunnable runnable, long delayedMillis) {
+        Log.d(TAG, "Posting " + runnable + " [delay " + delayedMillis + "]");
+        mHandler.post(runnable, delayedMillis, false);
+    }
+
+    /**
+     * Removes and cancels the specified {@code runnable} if it had not posted/started yet. Calling
+     * with null does nothing.
+     */
+    @Override
+    public void removeRunnable(@Nullable NamedRunnable runnable) {
+        if (runnable != null) {
+            // Removes any pending sent messages where what=WHAT and obj=runnable. We can't use
+            // removeCallbacks(runnable) because we're not posting the runnable directly, we're
+            // sending a Message with the runnable as its obj.
+            mHandler.removeMessages(WHAT, runnable);
+        }
+    }
+
+    /** Asserts that the current operation is being executed in the Event Loop's thread. */
+    @Override
+    public void checkThread() {
+
+        Thread currentThread = Looper.myLooper().getThread();
+        Thread expectedThread = mHandler.getLooper().getThread();
+        if (currentThread.getId() != expectedThread.getId()) {
+            throw new IllegalStateException(
+                    String.format(
+                            "This method must run in the EventLoop thread '%s (id: %s)'. "
+                                    + "Was called from thread '%s (id: %s)'.",
+                            expectedThread.getName(),
+                            expectedThread.getId(),
+                            currentThread.getName(),
+                            currentThread.getId()));
+        }
+
+    }
+
+    @Override
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    private void internalPostAndWait(final NamedRunnable runnable, boolean postToFront)
+            throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        NamedRunnable delegate =
+                new NamedRunnable(runnable.name) {
+                    @Override
+                    public void run() {
+                        try {
+                            runnable.run();
+                        } finally {
+                            latch.countDown();
+                        }
+                    }
+                };
+
+        Log.d(TAG, "Posting " + delegate + " and wait");
+        if (!mHandler.post(delegate, 0L, postToFront)) {
+            // Do not wait if delegate is not posted.
+            Log.d(TAG, delegate + " not posted");
+            latch.countDown();
+        }
+        latch.await();
+    }
+
+    /** Handler that executes code on a private event loop thread. */
+    private class MyHandler extends Handler {
+
+        MyHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            NamedRunnable runnable = (NamedRunnable) msg.obj;
+
+            if (mIsDestroyed) {
+                Log.w(TAG, "Runnable " + runnable
+                        + " attempted to run after the EventLoop was destroyed. Ignoring");
+                return;
+            }
+            Log.i(TAG, "Executing " + runnable);
+
+            // Did this runnable start much later than we expected it to? If so, then log.
+            long expectedStartTime = (long) msg.arg1 << 32 | (msg.arg2 & 0xFFFFFFFFL);
+            logIfExceedsThreshold(
+                    RUNNABLE_DELAY_THRESHOLD_MS, expectedStartTime, runnable, "was delayed for");
+
+            long startTimeMillis = SystemClock.elapsedRealtime();
+            try {
+                runnable.run();
+            } catch (Exception t) {
+                Log.e(TAG, runnable + "crashed.");
+                throw t;
+            } finally {
+                logIfExceedsThreshold(ELAPSED_THRESHOLD_MS, startTimeMillis, runnable, "ran for");
+            }
+        }
+
+        private boolean post(NamedRunnable runnable, long delayedMillis, boolean postToFront) {
+            if (mIsDestroyed) {
+                Log.w(TAG, runnable + " not posted since EventLoop is destroyed");
+                return false;
+            }
+            long expectedStartTime = SystemClock.elapsedRealtime() + delayedMillis;
+            int arg1 = (int) (expectedStartTime >> 32);
+            int arg2 = (int) expectedStartTime;
+            Message message = obtainMessage(WHAT, arg1, arg2, runnable /* obj */);
+            boolean sent =
+                    postToFront
+                            ? sendMessageAtFrontOfQueue(message)
+                            : sendMessageDelayed(message, delayedMillis);
+            if (!sent) {
+                Log.w(TAG, runnable + "not posted since looper is exiting");
+            }
+            return sent;
+        }
+
+        private void logIfExceedsThreshold(
+                long thresholdMillis, long startTimeMillis, NamedRunnable runnable,
+                String message) {
+            long elapsedMillis = SystemClock.elapsedRealtime() - startTimeMillis;
+            if (elapsedMillis > thresholdMillis) {
+                String elapsedFormatted =
+                        new SimpleDateFormat("mm:ss.SSS", Locale.US).format(elapsedMillis);
+                Log.w(TAG, runnable + " " + message + " " + elapsedFormatted);
+            }
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java b/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
new file mode 100644
index 0000000..578e3f6
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/eventloop/NamedRunnable.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 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.server.nearby.common.eventloop;
+
+/** A Runnable with a name, for logging purposes. */
+public abstract class NamedRunnable implements Runnable {
+    public final String name;
+
+    public NamedRunnable(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "Runnable[" + name + "]";
+    }
+}
diff --git a/nearby/tests/Android.bp b/nearby/tests/Android.bp
index 4007f7d..16e23c8 100644
--- a/nearby/tests/Android.bp
+++ b/nearby/tests/Android.bp
@@ -36,6 +36,9 @@
         "androidx.test.rules",
         "framework-nearby-pre-jarjar",
         "guava",
+        "mockito-robolectric-prebuilt",
+        "robolectric_android-all-stub",
+        "Robolectric_all-target",
         "libprotobuf-java-lite",
         "mockito-target",
         "platform-test-annotations",
diff --git a/nearby/tests/src/com/android/server/nearby/common/eventloop/EventLoopTest.java b/nearby/tests/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
new file mode 100644
index 0000000..844615f
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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.server.nearby.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class EventLoopTest {
+    private static final String TAG = "EventLoopTest";
+
+    private final EventLoop mEventLoop = EventLoop.newInstance(TAG);
+    private final List<Integer> mExecutedRunnables = new ArrayList<>();
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void remove() {
+        mEventLoop.postRunnable(new NumberedRunnable(0));
+        NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
+        mEventLoop.postRunnable(runnableToAddAndRemove);
+        mEventLoop.removeRunnable(runnableToAddAndRemove);
+        mEventLoop.postRunnable(new NumberedRunnable(2));
+
+        assertThat(mExecutedRunnables).containsExactly(0, 1);
+    }
+
+    @Test
+    public void isPosted() {
+        NumberedRunnable runnable = new NumberedRunnable(0);
+        mEventLoop.postRunnableDelayed(runnable, 10 * 1000L);
+        assertThat(mEventLoop.isPosted(runnable)).isTrue();
+        mEventLoop.removeRunnable(runnable);
+        assertThat(mEventLoop.isPosted(runnable)).isFalse();
+
+        // Let a runnable execute, then verify that it's not posted.
+        mEventLoop.postRunnable(runnable);
+        assertThat(mEventLoop.isPosted(runnable)).isTrue();
+    }
+
+    @Test
+    public void postAndWaitAfterDestroy() throws InterruptedException {
+        mEventLoop.destroy();
+        mEventLoop.postAndWait(new NumberedRunnable(0));
+
+        assertThat(mExecutedRunnables).isEmpty();
+    }
+
+
+    private class NumberedRunnable extends NamedRunnable {
+        private final int mId;
+
+        private NumberedRunnable(int id) {
+            super("NumberedRunnable:" + id);
+            this.mId = id;
+        }
+
+        @Override
+        public void run() {
+            // Note: when running in robolectric, this is not actually executed on a different
+            // thread, it's executed in the same thread the test runs in, so this is safe.
+            mExecutedRunnables.add(mId);
+        }
+    }
+}