From 08998b0076594a8eefa5730a599b50dcee2536cc Mon Sep 17 00:00:00 2001 From: Mis012 Date: Tue, 25 Jul 2023 14:26:29 +0200 Subject: [PATCH] make Looper, Handler, and MessageQueue work properly this for example makes Unity apps not steal the main thread, hanging Gtk. --- meson.build | 1 + src/api-impl-jni/android_os_MessageQueue.c | 62 ++ src/api-impl-jni/android_os_SystemClock.c | 10 + .../android_os_MessageQueue.h | 20 +- .../android_os_SystemClock.h | 8 + src/api-impl/android/app/Activity.java | 14 +- src/api-impl/android/content/Context.java | 20 +- src/api-impl/android/os/Bundle.java | 1 + src/api-impl/android/os/Handler.java | 48 +- src/api-impl/android/os/HandlerThread.java | 144 +++- src/api-impl/android/os/Looper.java | 86 +-- src/api-impl/android/os/MessageQueue.java | 12 +- src/api-impl/android/os/SystemClock.java | 45 +- src/api-impl/android/view/Choreographer.java | 653 +++++++++++++++++- src/libandroid/looper.c | 31 +- 15 files changed, 997 insertions(+), 158 deletions(-) create mode 100644 src/api-impl-jni/android_os_MessageQueue.c diff --git a/meson.build b/meson.build index 5f0a88c9..3638cf94 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,7 @@ libandroid_so = shared_library('android', [ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/egl/com_google_android_gles_jni_EGLImpl.c', 'src/api-impl-jni/android_os_Environment.c', + 'src/api-impl-jni/android_os_MessageQueue.c', 'src/api-impl-jni/android_os_SystemClock.c', 'src/api-impl-jni/android_view_Window.c', 'src/api-impl-jni/util.c', diff --git a/src/api-impl-jni/android_os_MessageQueue.c b/src/api-impl-jni/android_os_MessageQueue.c new file mode 100644 index 00000000..b48e47e3 --- /dev/null +++ b/src/api-impl-jni/android_os_MessageQueue.c @@ -0,0 +1,62 @@ +#include "defines.h" +#include "util.h" + +#include +#include +#include + +/* TODO put these in a header */ +typedef void ALooper; +ALooper * ALooper_prepare(void); +void ALooper_wake(ALooper *looper); +bool ALooper_isPolling(ALooper *looper); +int ALooper_pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData); + +struct native_message_queue { + ALooper *looper; + bool in_callback; +}; + +JNIEXPORT jlong JNICALL Java_android_os_MessageQueue_nativeInit(JNIEnv *env, jclass this) +{ + struct native_message_queue *message_queue = malloc(sizeof(struct native_message_queue)); + + message_queue->in_callback = false; + message_queue->looper = ALooper_prepare(); + + return _INTPTR(message_queue); +} + +JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativeDestroy(JNIEnv *env, jclass this, jlong ptr) +{ + struct native_message_queue *message_queue = _PTR(ptr); + + free(message_queue); +} + +JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativePollOnce(JNIEnv *env, jclass this, jlong ptr, jint timeout_millis) +{ + struct native_message_queue *message_queue = _PTR(ptr); + +// printf("Java_android_os_MessageQueue_nativePollOnce: entry (timeout: %d)\n", timeout_millis); + message_queue->in_callback = true; + ALooper_pollOnce(timeout_millis, NULL, NULL, NULL); + message_queue->in_callback = false; +// printf("Java_android_os_MessageQueue_nativePollOnce: exit\n"); + + /* TODO: what's with the exception stuff */ +} + +JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativeWake(JNIEnv *env, jclass this, jlong ptr) +{ + struct native_message_queue *message_queue = _PTR(ptr); + + ALooper_wake(message_queue->looper); +} + +JNIEXPORT jboolean JNICALL Java_android_os_MessageQueue_nativeIsIdling(JNIEnv *env, jclass this, jlong ptr) +{ + struct native_message_queue *message_queue = _PTR(ptr); + + return ALooper_isPolling(message_queue->looper); +} diff --git a/src/api-impl-jni/android_os_SystemClock.c b/src/api-impl-jni/android_os_SystemClock.c index 41b38c78..5f153bc9 100644 --- a/src/api-impl-jni/android_os_SystemClock.c +++ b/src/api-impl-jni/android_os_SystemClock.c @@ -1,3 +1,6 @@ +#include +#include + #include "generated_headers/android_os_SystemClock.h" JNIEXPORT jlong JNICALL Java_android_os_SystemClock_elapsedRealtime(JNIEnv *env, jclass this) @@ -5,3 +8,10 @@ JNIEXPORT jlong JNICALL Java_android_os_SystemClock_elapsedRealtime(JNIEnv *env, printf("FIXME: Java_android_os_SystemClock_elapsedRealtime: returning 0\n"); return 0; // FIXME } + +JNIEXPORT jlong JNICALL Java_android_os_SystemClock_uptimeMillis(JNIEnv *env, jclass this) +{ + struct timespec t; + clock_gettime(CLOCK_REALTIME, &t); + return t.tv_sec * 1000 + lround(t.tv_nsec / 1e6); +} diff --git a/src/api-impl-jni/generated_headers/android_os_MessageQueue.h b/src/api-impl-jni/generated_headers/android_os_MessageQueue.h index 99d334fe..18a7ea1f 100644 --- a/src/api-impl-jni/generated_headers/android_os_MessageQueue.h +++ b/src/api-impl-jni/generated_headers/android_os_MessageQueue.h @@ -10,42 +10,42 @@ extern "C" { /* * Class: android_os_MessageQueue * Method: nativeInit - * Signature: ()I + * Signature: ()J */ -JNIEXPORT jint JNICALL Java_android_os_MessageQueue_nativeInit +JNIEXPORT jlong JNICALL Java_android_os_MessageQueue_nativeInit (JNIEnv *, jclass); /* * Class: android_os_MessageQueue * Method: nativeDestroy - * Signature: (I)V + * Signature: (J)V */ JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativeDestroy - (JNIEnv *, jclass, jint); + (JNIEnv *, jclass, jlong); /* * Class: android_os_MessageQueue * Method: nativePollOnce - * Signature: (II)V + * Signature: (JI)V */ JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativePollOnce - (JNIEnv *, jclass, jint, jint); + (JNIEnv *, jclass, jlong, jint); /* * Class: android_os_MessageQueue * Method: nativeWake - * Signature: (I)V + * Signature: (J)V */ JNIEXPORT void JNICALL Java_android_os_MessageQueue_nativeWake - (JNIEnv *, jclass, jint); + (JNIEnv *, jclass, jlong); /* * Class: android_os_MessageQueue * Method: nativeIsIdling - * Signature: (I)Z + * Signature: (J)Z */ JNIEXPORT jboolean JNICALL Java_android_os_MessageQueue_nativeIsIdling - (JNIEnv *, jclass, jint); + (JNIEnv *, jclass, jlong); #ifdef __cplusplus } diff --git a/src/api-impl-jni/generated_headers/android_os_SystemClock.h b/src/api-impl-jni/generated_headers/android_os_SystemClock.h index 6d71ead0..002b83cd 100644 --- a/src/api-impl-jni/generated_headers/android_os_SystemClock.h +++ b/src/api-impl-jni/generated_headers/android_os_SystemClock.h @@ -15,6 +15,14 @@ extern "C" { JNIEXPORT jboolean JNICALL Java_android_os_SystemClock_setCurrentTimeMillis (JNIEnv *, jclass, jlong); +/* + * Class: android_os_SystemClock + * Method: uptimeMillis + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_android_os_SystemClock_uptimeMillis + (JNIEnv *, jclass); + /* * Class: android_os_SystemClock * Method: elapsedRealtime diff --git a/src/api-impl/android/app/Activity.java b/src/api-impl/android/app/Activity.java index 06039643..ddc48ca9 100644 --- a/src/api-impl/android/app/Activity.java +++ b/src/api-impl/android/app/Activity.java @@ -6,6 +6,8 @@ import android.content.Intent; import android.content.res.Configuration; import android.content.res.XmlResourceParser; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -102,6 +104,12 @@ public class Activity extends Context { protected void onCreate(Bundle savedInstanceState) { System.out.println("- onCreate - yay!"); + /* TODO: this probably belongs elsewhere, but this is our entry point for better or worse */ + Looper looper = Looper.myLooper(); + if(looper == null) { + Looper.prepareMainLooper(); + } + return; } @@ -212,7 +220,11 @@ public class Activity extends Context { } public final void runOnUiThread(Runnable action) { - action.run(); // FIXME: running synchronously for now + if(Looper.myLooper() == Looper.getMainLooper()) { + action.run(); + } else { + new Handler(Looper.getMainLooper()).post(action); + } } protected void onActivityResult(int requestCode, int resultCode, Intent data) {} diff --git a/src/api-impl/android/content/Context.java b/src/api-impl/android/content/Context.java index 8337c645..cf09547b 100644 --- a/src/api-impl/android/content/Context.java +++ b/src/api-impl/android/content/Context.java @@ -33,6 +33,7 @@ import android.view.WindowManager; import android.view.WindowManagerImpl; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; public class Context extends Object { @@ -138,8 +139,14 @@ public class Context extends Object { } public Looper getMainLooper() { - System.out.println("returning the main Looper, most definitely doing just that!"); - return new Looper(); + /* TODO: this is not what AOSP does, which could be a problem */ + Looper looper = Looper.myLooper(); + if(looper == null) { + Looper.prepare(); + looper = Looper.myLooper(); + } + + return looper; } public String getPackageName() { @@ -251,9 +258,12 @@ public class Context extends Object { return new ComponentName("", ""); } - // FIXME - it should be *trivial* to do actually implement this - public FileInputStream openFileInput(String name) { - return null; + // TODO: do these both work? make them look more alike + public FileInputStream openFileInput(String name) throws FileNotFoundException { + System.out.println("openFileInput called for: '" + name + "'"); + File file = new File(getFilesDir(), name); + + return new FileInputStream(file); } public FileOutputStream openFileOutput(String name, int mode) throws java.io.FileNotFoundException { diff --git a/src/api-impl/android/os/Bundle.java b/src/api-impl/android/os/Bundle.java index 24539c57..98d47351 100644 --- a/src/api-impl/android/os/Bundle.java +++ b/src/api-impl/android/os/Bundle.java @@ -738,6 +738,7 @@ public final class Bundle implements Cloneable { */ public boolean getBoolean(String key, boolean defaultValue) { Object o = mMap.get(key); + System.out.println("bundle.getBoolean(" + key + ", " + defaultValue + ") called"); if (o == null) { return defaultValue; } diff --git a/src/api-impl/android/os/Handler.java b/src/api-impl/android/os/Handler.java index 9b4d9ad2..baa23f6a 100644 --- a/src/api-impl/android/os/Handler.java +++ b/src/api-impl/android/os/Handler.java @@ -225,7 +225,7 @@ public class Handler { */ public Handler(Looper looper, Callback callback, boolean async) { mLooper = looper; - mQueue = null /*looper.mQueue*/; + mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async; } @@ -316,9 +316,7 @@ public class Handler { * looper processing the message queue is exiting. */ public final boolean post(Runnable r) { - // return sendMessageDelayed(getPostMessage(r), 0); - r.run(); - return true; + return sendMessageDelayed(getPostMessage(r), 0); } /** @@ -571,29 +569,14 @@ public class Handler { * occurs then the message will be dropped. */ public boolean sendMessageAtTime(Message msg, long uptimeMillis) { - /* MessageQueue queue = mQueue; - if (queue == null) { - RuntimeException e = new RuntimeException( - this + " sendMessageAtTime() called with no mQueue"); - Log.w("Looper", e.getMessage(), e); - return false; - } - return enqueueMessage(queue, msg, uptimeMillis);*/ - if (mCallback != null) { - // System.out.println("Handler.sendMessageAtTime: directly calling mCallback.handleMessage)"); - if (msg.callback != null) { - msg.callback.run(); - } - return mCallback.handleMessage(msg); - } else { - // System.out.println("Handler.sendMessageAtTime: not directly calling mCallback.handleMessage - mCallback is null)"); - /* do this in this case as well? - if(msg.callback != null) { - msg.callback.run(); - } - */ - return true; // false? + MessageQueue queue = mQueue; + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; } + return enqueueMessage(queue, msg, uptimeMillis); } /** @@ -609,15 +592,14 @@ public class Handler { * looper processing the message queue is exiting. */ public final boolean sendMessageAtFrontOfQueue(Message msg) { - /*MessageQueue queue = mQueue; + MessageQueue queue = mQueue; if (queue == null) { - RuntimeException e = new RuntimeException( - this + " sendMessageAtTime() called with no mQueue"); - Log.w("Looper", e.getMessage(), e); - return false; + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; } - return enqueueMessage(queue, msg, 0);*/ - return true; + return enqueueMessage(queue, msg, 0); } private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { diff --git a/src/api-impl/android/os/HandlerThread.java b/src/api-impl/android/os/HandlerThread.java index cede5f90..4923cdfb 100644 --- a/src/api-impl/android/os/HandlerThread.java +++ b/src/api-impl/android/os/HandlerThread.java @@ -1,19 +1,149 @@ +/* + * Copyright (C) 2006 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.os; +/** + * Handy class for starting a new thread that has a looper. The looper can then be + * used to create handler classes. Note that start() must still be called. + */ public class HandlerThread extends Thread { - String name; - - public HandlerThread() {} + int mPriority; + int mTid = -1; + Looper mLooper; public HandlerThread(String name) { - this.name = name; + super(name); + mPriority = Process.THREAD_PRIORITY_DEFAULT; } - public void start() { - // if(name.equals("FlurryAgent")) { return; } + /** + * Constructs a HandlerThread. + * @param name + * @param priority The priority to run the thread at. The value supplied must be from + * {@link android.os.Process} and not from java.lang.Thread. + */ + public HandlerThread(String name, int priority) { + super(name); + mPriority = priority; } + /** + * Call back method that can be explicitly overridden if needed to execute some + * setup before Looper loops. + */ + protected void onLooperPrepared() { + } + + @Override + public void run() { + mTid = Process.myTid(); + Looper.prepare(); + synchronized (this) { + mLooper = Looper.myLooper(); + notifyAll(); + } + Process.setThreadPriority(mPriority); + onLooperPrepared(); + Looper.loop(); + mTid = -1; + } + + /** + * This method returns the Looper associated with this thread. If this thread not been started + * or for any reason is isAlive() returns false, this method will return null. If this thread + * has been started, this method will block until the looper has been initialized. + * @return The looper. + */ public Looper getLooper() { - return new Looper(); + if (!isAlive()) { + return null; + } + + // If the thread has been started, wait until the looper has been created. + synchronized (this) { + while (isAlive() && mLooper == null) { + try { + wait(); + } catch (InterruptedException e) { + } + } + } + return mLooper; + } + + /** + * Quits the handler thread's looper. + *

+ * Causes the handler thread's looper to terminate without processing any + * more messages in the message queue. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ * Using this method may be unsafe because some messages may not be delivered + * before the looper terminates. Consider using {@link #quitSafely} instead to ensure + * that all pending work is completed in an orderly manner. + *

+ * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + * + * @see #quitSafely + */ + public boolean quit() { + Looper looper = getLooper(); + if (looper != null) { + looper.quit(); + return true; + } + return false; + } + + /** + * Quits the handler thread's looper safely. + *

+ * Causes the handler thread's looper to terminate as soon as all remaining messages + * in the message queue that are already due to be delivered have been handled. + * Pending delayed messages with due times in the future will not be delivered. + *

+ * Any attempt to post messages to the queue after the looper is asked to quit will fail. + * For example, the {@link Handler#sendMessage(Message)} method will return false. + *

+ * If the thread has not been started or has finished (that is if + * {@link #getLooper} returns null), then false is returned. + * Otherwise the looper is asked to quit and true is returned. + *

+ * + * @return True if the looper looper has been asked to quit or false if the + * thread had not yet started running. + */ + public boolean quitSafely() { + Looper looper = getLooper(); + if (looper != null) { + looper.quitSafely(); + return true; + } + return false; + } + + /** + * Returns the identifier of this thread. See Process.myTid(). + */ + public int getThreadId() { + return mTid; } } diff --git a/src/api-impl/android/os/Looper.java b/src/api-impl/android/os/Looper.java index 4ec01da9..48e28c18 100644 --- a/src/api-impl/android/os/Looper.java +++ b/src/api-impl/android/os/Looper.java @@ -53,13 +53,6 @@ import android.util.Printer; public final class Looper { private static final String TAG = "Looper"; - // FIXME - public Looper() { - mQueue = null; - mThread = Thread.currentThread(); - System.out.println("making a fake Looper object, let's hope noone tries to actually use it"); - } - // sThreadLocal.get() will return null unless you've called prepare(). static final ThreadLocal sThreadLocal = new ThreadLocal(); private static Looper sMainLooper; // guarded by Looper.class @@ -84,7 +77,7 @@ public final class Looper { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } - sThreadLocal.set(new Looper(/*quitAllowed*/)); + sThreadLocal.set(new Looper(quitAllowed)); } /** @@ -107,10 +100,9 @@ public final class Looper { * Returns the application's main looper, which lives in the main thread of the application. */ public static Looper getMainLooper() { - return new Looper(); - /* synchronized (Looper.class) { - return sMainLooper; - }*/ + synchronized (Looper.class) { + return sMainLooper; + } } /** @@ -118,51 +110,46 @@ public final class Looper { * {@link #quit()} to end the loop. */ public static void loop() { - System.out.println("oops, Looper.loop called... and we don't implement that"); - /*final Looper me = myLooper(); + final Looper me = myLooper(); if (me == null) { - throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. - // Binder.clearCallingIdentity(); - // final long ident = Binder.clearCallingIdentity(); +// Binder.clearCallingIdentity(); +// final long ident = Binder.clearCallingIdentity(); for (;;) { - Message msg = queue.next(); // might block - if (msg == null) { - // No message indicates that the message queue is quitting. - return; - } + Message msg = queue.next(); // might block + if (msg == null) { + // No message indicates that the message queue is quitting. + return; + } - // This must be in a local variable, in case a UI event sets the logger - Printer logging = me.mLogging; - if (logging != null) { - logging.println(">>>>> Dispatching to " + msg.target + " " + - msg.callback + ": " + msg.what); - } + // This must be in a local variable, in case a UI event sets the logger + Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } - msg.target.dispatchMessage(msg); + msg.target.dispatchMessage(msg); - if (logging != null) { - logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); - } + if (logging != null) { + logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); + } - // Make sure that during the course of dispatching the - // identity of the thread wasn't corrupted. - // final long newIdent = Binder.clearCallingIdentity(); - /* if (ident != newIdent) { - Log.wtf(TAG, "Thread identity changed from 0x" - + Long.toHexString(ident) + " to 0x" - + Long.toHexString(newIdent) + " while dispatching to " - + msg.target.getClass().getName() + " " - + msg.callback + " what=" + msg.what); - }* / - - msg.recycle(); - }*/ + // Make sure that during the course of dispatching the + // identity of the thread wasn't corrupted. +/* final long newIdent = Binder.clearCallingIdentity(); + if (ident != newIdent) { + Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); + } +*/ + msg.recycle(); + } } /** @@ -170,11 +157,7 @@ public final class Looper { * null if the calling thread is not associated with a Looper. */ public static Looper myLooper() { - return new Looper(); - /* if(sThreadLocal.get() == null) { - prepare(false); - } - return sThreadLocal.get();*/ + return sThreadLocal.get(); } /** @@ -293,8 +276,7 @@ public final class Looper { * Return the Thread associated with this Looper. */ public Thread getThread() { - return Thread.currentThread(); // ugly hack - // return mThread; + return mThread; } /** diff --git a/src/api-impl/android/os/MessageQueue.java b/src/api-impl/android/os/MessageQueue.java index 40dd0fcd..e80acde2 100644 --- a/src/api-impl/android/os/MessageQueue.java +++ b/src/api-impl/android/os/MessageQueue.java @@ -34,7 +34,7 @@ public final class MessageQueue { private final boolean mQuitAllowed; @SuppressWarnings("unused") - private int mPtr; // used by native code + private long mPtr; // used by native code Message mMessages; private final ArrayList mIdleHandlers = new ArrayList(); @@ -48,11 +48,11 @@ public final class MessageQueue { // Barriers are indicated by messages with a null target whose arg1 field carries the token. private int mNextBarrierToken; - private native static int nativeInit(); - private native static void nativeDestroy(int ptr); - private native static void nativePollOnce(int ptr, int timeoutMillis); - private native static void nativeWake(int ptr); - private native static boolean nativeIsIdling(int ptr); + private native static long nativeInit(); + private native static void nativeDestroy(long ptr); + private native static void nativePollOnce(long ptr, int timeoutMillis); + private native static void nativeWake(long ptr); + private native static boolean nativeIsIdling(long ptr); /** * Callback interface for discovering when a thread is going to block diff --git a/src/api-impl/android/os/SystemClock.java b/src/api-impl/android/os/SystemClock.java index 928da5d6..4d677798 100644 --- a/src/api-impl/android/os/SystemClock.java +++ b/src/api-impl/android/os/SystemClock.java @@ -105,28 +105,25 @@ public final class SystemClock { * @param ms to sleep before returning, in milliseconds of uptime. */ public static void sleep(long ms) { - System.out.println("!!! sleep(...) doesn't work, need to implement uptimeMillis"); - /* - long start = uptimeMillis(); - long duration = ms; - boolean interrupted = false; - do { - try { - Thread.sleep(duration); - } - catch (InterruptedException e) { - interrupted = true; - } - duration = start + ms - uptimeMillis(); - } while (duration > 0); - - if (interrupted) { - // Important: we don't want to quietly eat an interrupt() event, - // so we make sure to re-interrupt the thread so that the next - // call to Thread.sleep() or Object.wait() will be interrupted. - Thread.currentThread().interrupt(); - } - */} + long start = uptimeMillis(); + long duration = ms; + boolean interrupted = false; + do { + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + interrupted = true; + } + duration = start + ms - uptimeMillis(); + } while (duration > 0); + + if (interrupted) { + // Important: we don't want to quietly eat an interrupt() event, + // so we make sure to re-interrupt the thread so that the next + // call to Thread.sleep() or Object.wait() will be interrupted. + Thread.currentThread().interrupt(); + } + } /** * Sets the current wall time, in milliseconds. Requires the calling @@ -141,9 +138,7 @@ public final class SystemClock { * * @return milliseconds of non-sleep uptime since boot. */ - /*native */ public static long uptimeMillis() { - return 654000; // FIXME - } + native public static long uptimeMillis(); /** * Returns milliseconds since boot, including time spent in sleep. diff --git a/src/api-impl/android/view/Choreographer.java b/src/api-impl/android/view/Choreographer.java index d5683fd7..389e996a 100644 --- a/src/api-impl/android/view/Choreographer.java +++ b/src/api-impl/android/view/Choreographer.java @@ -1,25 +1,648 @@ +/* + * Copyright (C) 2011 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.view; - +//import android.hardware.display.DisplayManagerGlobal; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +//import android.os.SystemProperties; +import android.util.Log; +/** + * Coordinates the timing of animations, input and drawing. + *

+ * The choreographer receives timing pulses (such as vertical synchronization) + * from the display subsystem then schedules work to occur as part of rendering + * the next display frame. + *

+ * Applications typically interact with the choreographer indirectly using + * higher level abstractions in the animation framework or the view hierarchy. + * Here are some examples of things you can do using the higher-level APIs. + *

+ *
    + *
  • To post an animation to be processed on a regular time basis synchronized with + * display frame rendering, use {@link android.animation.ValueAnimator#start}.
  • + *
  • To post a {@link Runnable} to be invoked once at the beginning of the next display + * frame, use {@link View#postOnAnimation}.
  • + *
  • To post a {@link Runnable} to be invoked once at the beginning of the next display + * frame after a delay, use {@link View#postOnAnimationDelayed}.
  • + *
  • To post a call to {@link View#invalidate()} to occur once at the beginning of the + * next display frame, use {@link View#postInvalidateOnAnimation()} or + * {@link View#postInvalidateOnAnimation(int, int, int, int)}.
  • + *
  • To ensure that the contents of a {@link View} scroll smoothly and are drawn in + * sync with display frame rendering, do nothing. This already happens automatically. + * {@link View#onDraw} will be called at the appropriate time.
  • + *
+ *

+ * However, there are a few cases where you might want to use the functions of the + * choreographer directly in your application. Here are some examples. + *

+ *
    + *
  • If your application does its rendering in a different thread, possibly using GL, + * or does not use the animation framework or view hierarchy at all + * and you want to ensure that it is appropriately synchronized with the display, then use + * {@link Choreographer#postFrameCallback}.
  • + *
  • ... and that's about it.
  • + *
+ *

+ * Each {@link Looper} thread has its own choreographer. Other threads can + * post callbacks to run on the choreographer but they will run on the {@link Looper} + * to which the choreographer belongs. + *

+ */ public final class Choreographer { - public static interface FrameCallback { - public void doFrame(long frametime_in_nanoseconds); + private static final String TAG = "Choreographer"; + private static final boolean DEBUG = false; + // The default amount of time in ms between animation frames. + // When vsync is not enabled, we want to have some idea of how long we should + // wait before posting the next animation message. It is important that the + // default value be less than the true inter-frame delay on all devices to avoid + // situations where we might skip frames by waiting too long (we must compensate + // for jitter and hardware variations). Regardless of this value, the animation + // and display loop is ultimately rate-limited by how fast new graphics buffers can + // be dequeued. + private static final long DEFAULT_FRAME_DELAY = 10; + // The number of milliseconds between animation frames. + private static volatile long sFrameDelay = DEFAULT_FRAME_DELAY; + // Thread local storage for the choreographer. + private static final ThreadLocal sThreadInstance = + new ThreadLocal() { + @Override + protected Choreographer initialValue() { + Looper looper = Looper.myLooper(); + if (looper == null) { + throw new IllegalStateException("The current thread must have a looper!"); + } + return new Choreographer(looper); + } + }; + // Enable/disable vsync for animations and drawing. + private static final boolean USE_VSYNC = false; + // Enable/disable using the frame time instead of returning now. + private static final boolean USE_FRAME_TIME = false; + // Set a limit to warn about skipped frames. + // Skipped frames imply jank. + private static final int SKIPPED_FRAME_WARNING_LIMIT = 30; + private static final long NANOS_PER_MS = 1000000; + private static final int MSG_DO_FRAME = 0; + private static final int MSG_DO_SCHEDULE_VSYNC = 1; + private static final int MSG_DO_SCHEDULE_CALLBACK = 2; + // All frame callbacks posted by applications have this token. + private static final Object FRAME_CALLBACK_TOKEN = new Object() { + public String toString() { return "FRAME_CALLBACK_TOKEN"; } + }; + private final Object mLock = new Object(); + private final Looper mLooper; + private final FrameHandler mHandler; + // The display event receiver can only be accessed by the looper thread to which + // it is attached. We take care to ensure that we post message to the looper + // if appropriate when interacting with the display event receiver. + private CallbackRecord mCallbackPool; + private final CallbackQueue[] mCallbackQueues; + private boolean mFrameScheduled; + private boolean mCallbacksRunning; + private long mLastFrameTimeNanos; + private long mFrameIntervalNanos; + /** + * Callback type: Input callback. Runs first. + * @hide + */ + public static final int CALLBACK_INPUT = 0; + /** + * Callback type: Animation callback. Runs before traversals. + * @hide + */ + public static final int CALLBACK_ANIMATION = 1; + /** + * Callback type: Traversal callback. Handles layout and draw. Runs last + * after all other asynchronous messages have been handled. + * @hide + */ + public static final int CALLBACK_TRAVERSAL = 2; + private static final int CALLBACK_LAST = CALLBACK_TRAVERSAL; + private Choreographer(Looper looper) { + mLooper = looper; + mHandler = new FrameHandler(looper); + mLastFrameTimeNanos = Long.MIN_VALUE; + mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); + mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1]; + for (int i = 0; i <= CALLBACK_LAST; i++) { + mCallbackQueues[i] = new CallbackQueue(); + } } - + private static float getRefreshRate() { +/* DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo( + Display.DEFAULT_DISPLAY);*/ + return 60/*di.refreshRate*/; // FIXME + } + /** + * Gets the choreographer for the calling thread. Must be called from + * a thread that already has a {@link android.os.Looper} associated with it. + * + * @return The choreographer for this thread. + * @throws IllegalStateException if the thread does not have a looper. + */ public static Choreographer getInstance() { - return new Choreographer(); + return sThreadInstance.get(); } - - public void postFrameCallback(Choreographer.FrameCallback callback) { + /** + * The amount of time, in milliseconds, between each frame of the animation. + *

+ * This is a requested time that the animation will attempt to honor, but the actual delay + * between frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + *

+ * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + *

+ * + * @return the requested time between frames, in milliseconds + * @hide + */ + public static long getFrameDelay() { + return sFrameDelay; + } + /** + * The amount of time, in milliseconds, between each frame of the animation. + *

+ * This is a requested time that the animation will attempt to honor, but the actual delay + * between frames may be different, depending on system load and capabilities. This is a static + * function because the same delay will be applied to all animations, since they are all + * run off of a single timing loop. + *

+ * The frame delay may be ignored when the animation system uses an external timing + * source, such as the display refresh rate (vsync), to govern animations. + *

+ * + * @param frameDelay the requested time between frames, in milliseconds + * @hide + */ + public static void setFrameDelay(long frameDelay) { + sFrameDelay = frameDelay; + } + /** + * Subtracts typical frame delay time from a delay interval in milliseconds. + *

+ * This method can be used to compensate for animation delay times that have baked + * in assumptions about the frame delay. For example, it's quite common for code to + * assume a 60Hz frame time and bake in a 16ms delay. When we call + * {@link #postAnimationCallbackDelayed} we want to know how long to wait before + * posting the animation callback but let the animation timer take care of the remaining + * frame delay time. + *

+ * This method is somewhat conservative about how much of the frame delay it + * subtracts. It uses the same value returned by {@link #getFrameDelay} which by + * default is 10ms even though many parts of the system assume 16ms. Consequently, + * we might still wait 6ms before posting an animation callback that we want to run + * on the next frame, but this is much better than waiting a whole 16ms and likely + * missing the deadline. + *

+ * + * @param delayMillis The original delay time including an assumed frame delay. + * @return The adjusted delay time with the assumed frame delay subtracted out. + * @hide + */ + public static long subtractFrameDelay(long delayMillis) { + final long frameDelay = sFrameDelay; + return delayMillis <= frameDelay ? 0 : delayMillis - frameDelay; + } + /** + * Posts a callback to run on the next frame. + *

+ * The callback runs once then is automatically removed. + *

+ * + * @param callbackType The callback type. + * @param action The callback action to run during the next frame. + * @param token The callback token, or null if none. + * + * @see #removeCallbacks + * @hide + */ + public void postCallback(int callbackType, Runnable action, Object token) { + postCallbackDelayed(callbackType, action, token, 0); + } + /** + * Posts a callback to run on the next frame after the specified delay. + *

+ * The callback runs once then is automatically removed. + *

+ * + * @param callbackType The callback type. + * @param action The callback action to run during the next frame after the specified delay. + * @param token The callback token, or null if none. + * @param delayMillis The delay time in milliseconds. + * + * @see #removeCallback + * @hide + */ + public void postCallbackDelayed(int callbackType, + Runnable action, Object token, long delayMillis) { + if (action == null) { + throw new IllegalArgumentException("action must not be null"); + } + if (callbackType < 0 || callbackType > CALLBACK_LAST) { + throw new IllegalArgumentException("callbackType is invalid"); + } + postCallbackDelayedInternal(callbackType, action, token, delayMillis); + } + private void postCallbackDelayedInternal(int callbackType, + Object action, Object token, long delayMillis) { + if (DEBUG) { + Log.d(TAG, "PostCallback: type=" + callbackType + ", action=" + action + ", token=" + token + ", delayMillis=" + delayMillis); + } + synchronized (mLock) { + final long now = SystemClock.uptimeMillis(); + final long dueTime = now + delayMillis; + mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); + if (dueTime <= now) { + scheduleFrameLocked(now); + } else { + Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); + msg.arg1 = callbackType; + msg.setAsynchronous(true); + mHandler.sendMessageAtTime(msg, dueTime); + } + } + } + /** + * Removes callbacks that have the specified action and token. + * + * @param callbackType The callback type. + * @param action The action property of the callbacks to remove, or null to remove + * callbacks with any action. + * @param token The token property of the callbacks to remove, or null to remove + * callbacks with any token. + * + * @see #postCallback + * @see #postCallbackDelayed + * @hide + */ + public void removeCallbacks(int callbackType, Runnable action, Object token) { + if (callbackType < 0 || callbackType > CALLBACK_LAST) { + throw new IllegalArgumentException("callbackType is invalid"); + } + removeCallbacksInternal(callbackType, action, token); + } + private void removeCallbacksInternal(int callbackType, Object action, Object token) { + if (DEBUG) { + Log.d(TAG, "RemoveCallbacks: type=" + callbackType + ", action=" + action + ", token=" + token); + } + synchronized (mLock) { + mCallbackQueues[callbackType].removeCallbacksLocked(action, token); + if (action != null && token == null) { + mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action); + } + } + } + /** + * Posts a frame callback to run on the next frame. + *

+ * The callback runs once then is automatically removed. + *

+ * + * @param callback The frame callback to run during the next frame. + * + * @see #postFrameCallbackDelayed + * @see #removeFrameCallback + */ + public void postFrameCallback(FrameCallback callback) { postFrameCallbackDelayed(callback, 0); } + /** + * Posts a frame callback to run on the next frame after the specified delay. + *

+ * The callback runs once then is automatically removed. + *

+ * + * @param callback The frame callback to run during the next frame. + * @param delayMillis The delay time in milliseconds. + * + * @see #postFrameCallback + * @see #removeFrameCallback + */ + public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + postCallbackDelayedInternal(CALLBACK_ANIMATION, + callback, FRAME_CALLBACK_TOKEN, delayMillis); + } + /** + * Removes a previously posted frame callback. + * + * @param callback The frame callback to remove. + * + * @see #postFrameCallback + * @see #postFrameCallbackDelayed + */ + public void removeFrameCallback(FrameCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + removeCallbacksInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN); + } + /** + * Gets the time when the current frame started. + *

+ * This method provides the time in nanoseconds when the frame started being rendered. + * The frame time provides a stable time base for synchronizing animations + * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()} + * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame + * time helps to reduce inter-frame jitter because the frame time is fixed at the time + * the frame was scheduled to start, regardless of when the animations or drawing + * callback actually runs. All callbacks that run as part of rendering a frame will + * observe the same frame time so using the frame time also helps to synchronize effects + * that are performed by different callbacks. + *

+ * Please note that the framework already takes care to process animations and + * drawing using the frame time as a stable time base. Most applications should + * not need to use the frame time information directly. + *

+ * This method should only be called from within a callback. + *

+ * + * @return The frame start time, in the {@link SystemClock#uptimeMillis()} time base. + * + * @throws IllegalStateException if no frame is in progress. + * @hide + */ + public long getFrameTime() { + return getFrameTimeNanos() / NANOS_PER_MS; + } + /** + * Same as {@link #getFrameTime()} but with nanosecond precision. + * + * @return The frame start time, in the {@link System#nanoTime()} time base. + * + * @throws IllegalStateException if no frame is in progress. + * @hide + */ + public long getFrameTimeNanos() { + synchronized (mLock) { + if (!mCallbacksRunning) { + throw new IllegalStateException("This method must only be called as " + + "part of a callback while a frame is in progress."); + } + return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime(); + } + } + private void scheduleFrameLocked(long now) { + if (!mFrameScheduled) { + mFrameScheduled = true; + final long nextFrameTime = Math.max( + mLastFrameTimeNanos / NANOS_PER_MS + sFrameDelay, now); + if (DEBUG) { + Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms."); + } + Message msg = mHandler.obtainMessage(MSG_DO_FRAME); + msg.setAsynchronous(true); + mHandler.sendMessageAtTime(msg, nextFrameTime); + } + } + void doFrame(long frameTimeNanos, int frame) { + final long startNanos; + synchronized (mLock) { + if (!mFrameScheduled) { + return; // no work to do + } + startNanos = System.nanoTime(); + final long jitterNanos = startNanos - frameTimeNanos; + if (jitterNanos >= mFrameIntervalNanos) { + final long skippedFrames = jitterNanos / mFrameIntervalNanos; + if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { + Log.i(TAG, "Skipped " + skippedFrames + " frames! " + + "The application may be doing too much work on its main thread."); + } + final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; + if (DEBUG) { + Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms " + + "which is more than the frame interval of " + (mFrameIntervalNanos * 0.000001f) + " ms! " + + "Skipping " + skippedFrames + " frames and setting frame " + + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past."); + } + frameTimeNanos = startNanos - lastFrameOffset; + } + if (frameTimeNanos < mLastFrameTimeNanos) { + if (DEBUG) { + Log.d(TAG, "Frame time appears to be going backwards. May be due to a " + + "previously skipped frame. Waiting for next vsync."); + } +// scheduleVsyncLocked(); + return; + } + mFrameScheduled = false; + mLastFrameTimeNanos = frameTimeNanos; + } + doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); + doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); + doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); + if (DEBUG) { + final long endNanos = System.nanoTime(); + Log.d(TAG, "Frame " + frame + ": Finished, took " + (endNanos - startNanos) * 0.000001f + " ms, latency " + (startNanos - frameTimeNanos) * 0.000001f + " ms."); + } + } + void doCallbacks(int callbackType, long frameTimeNanos) { + CallbackRecord callbacks; + synchronized (mLock) { + // We use "now" to determine when callbacks become due because it's possible + // for earlier processing phases in a frame to post callbacks that should run + // in a following phase, such as an input event that causes an animation to start. + final long now = SystemClock.uptimeMillis(); + callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now); + if (callbacks == null) { + return; + } + mCallbacksRunning = true; + } + try { + for (CallbackRecord c = callbacks; c != null; c = c.next) { + if (DEBUG) { + Log.d(TAG, "RunCallback: type=" + callbackType + ", action=" + c.action + ", token=" + c.token + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime)); + } + c.run(frameTimeNanos); + } + } finally { + synchronized (mLock) { + mCallbacksRunning = false; + do { + final CallbackRecord next = callbacks.next; + recycleCallbackLocked(callbacks); + callbacks = next; + } while (callbacks != null); + } + } + } + void doScheduleCallback(int callbackType) { + synchronized (mLock) { + if (!mFrameScheduled) { + final long now = SystemClock.uptimeMillis(); + if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) { + scheduleFrameLocked(now); + } + } + } + } + private boolean isRunningOnLooperThreadLocked() { + return Looper.myLooper() == mLooper; + } + private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) { + CallbackRecord callback = mCallbackPool; + if (callback == null) { + callback = new CallbackRecord(); + } else { + mCallbackPool = callback.next; + callback.next = null; + } + callback.dueTime = dueTime; + callback.action = action; + callback.token = token; + return callback; + } + private void recycleCallbackLocked(CallbackRecord callback) { + callback.action = null; + callback.token = null; + callback.next = mCallbackPool; + mCallbackPool = callback; + } + /** + * Implement this interface to receive a callback when a new display frame is + * being rendered. The callback is invoked on the {@link Looper} thread to + * which the {@link Choreographer} is attached. + */ + public interface FrameCallback { + /** + * Called when a new display frame is being rendered. + *

+ * This method provides the time in nanoseconds when the frame started being rendered. + * The frame time provides a stable time base for synchronizing animations + * and drawing. It should be used instead of {@link SystemClock#uptimeMillis()} + * or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame + * time helps to reduce inter-frame jitter because the frame time is fixed at the time + * the frame was scheduled to start, regardless of when the animations or drawing + * callback actually runs. All callbacks that run as part of rendering a frame will + * observe the same frame time so using the frame time also helps to synchronize effects + * that are performed by different callbacks. + *

+ * Please note that the framework already takes care to process animations and + * drawing using the frame time as a stable time base. Most applications should + * not need to use the frame time information directly. + *

+ * + * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, + * in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000} + * to convert it to the {@link SystemClock#uptimeMillis()} time base. + */ + public void doFrame(long frameTimeNanos); + } + private final class FrameHandler extends Handler { + public FrameHandler(Looper looper) { + super(looper); + } + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DO_FRAME: + doFrame(System.nanoTime(), 0); + break; + case MSG_DO_SCHEDULE_VSYNC: + Log.d(TAG, "VSYNC not supported!"); +// doScheduleVsync(); + break; + case MSG_DO_SCHEDULE_CALLBACK: + doScheduleCallback(msg.arg1); + break; + } + } + } - public void postFrameCallbackDelayed(final Choreographer.FrameCallback callback, long delayMillis) { - // TODO - do the delay part - // NOTE: if we do this synchronously, it gets stuck - Thread async = new Thread(new Runnable() { - public void run() { - callback.doFrame(System.nanoTime()); - } }); - async.start(); + private static final class CallbackRecord { + public CallbackRecord next; + public long dueTime; + public Object action; // Runnable or FrameCallback + public Object token; + public void run(long frameTimeNanos) { + if (token == FRAME_CALLBACK_TOKEN) { + ((FrameCallback)action).doFrame(frameTimeNanos); + } else { + ((Runnable)action).run(); + } + } + } + private final class CallbackQueue { + private CallbackRecord mHead; + public boolean hasDueCallbacksLocked(long now) { + return mHead != null && mHead.dueTime <= now; + } + public CallbackRecord extractDueCallbacksLocked(long now) { + CallbackRecord callbacks = mHead; + if (callbacks == null || callbacks.dueTime > now) { + return null; + } + CallbackRecord last = callbacks; + CallbackRecord next = last.next; + while (next != null) { + if (next.dueTime > now) { + last.next = null; + break; + } + last = next; + next = next.next; + } + mHead = next; + return callbacks; + } + public void addCallbackLocked(long dueTime, Object action, Object token) { + CallbackRecord callback = obtainCallbackLocked(dueTime, action, token); + CallbackRecord entry = mHead; + if (entry == null) { + mHead = callback; + return; + } + if (dueTime < entry.dueTime) { + callback.next = entry; + mHead = callback; + return; + } + while (entry.next != null) { + if (dueTime < entry.next.dueTime) { + callback.next = entry.next; + break; + } + entry = entry.next; + } + entry.next = callback; + } + public void removeCallbacksLocked(Object action, Object token) { + CallbackRecord predecessor = null; + for (CallbackRecord callback = mHead; callback != null;) { + final CallbackRecord next = callback.next; + if ((action == null || callback.action == action) && (token == null || callback.token == token)) { + if (predecessor != null) { + predecessor.next = next; + } else { + mHead = next; + } + recycleCallbackLocked(callback); + } else { + predecessor = callback; + } + callback = next; + } + } } } diff --git a/src/libandroid/looper.c b/src/libandroid/looper.c index cf22c546..aad25a47 100644 --- a/src/libandroid/looper.c +++ b/src/libandroid/looper.c @@ -1,3 +1,4 @@ +#include #include #include @@ -37,7 +38,7 @@ void ALooper_release(ALooper* looper) { _ZNK7android7RefBase9decStrongEPKv(looper, (void*)ALooper_acquire); } -int _ZN7android6Looper7pollAllEiPiS1_PPv(ALooper *this, int timeoutMillis, int* outFd, int* outEvents, void** outData); +int _ZN7android6Looper7pollAllEiPiS1_PPv(ALooper *this, int timeoutMillis, int *outFd, int *outEvents, void **outData); int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData) { ALooper *looper = ALooper_forThread(); @@ -49,12 +50,34 @@ int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outDat return _ZN7android6Looper7pollAllEiPiS1_PPv(looper, timeoutMillis, outFd, outEvents, outData); } -int _ZN7android6Looper5addFdEiiiPFiiiPvES1_(ALooper *this, int fd, int ident, int events, Looper_callbackFunc callback, void* data); +int _ZN7android6Looper8pollOnceEiPiS1_PPv(ALooper *this, int timeoutMillis, int *outFd, int *outEvents, void **outData); +int ALooper_pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) +{ + ALooper *looper = ALooper_forThread(); + if(!looper) { + fprintf(stderr, "ALooper_pollAll: ALooper_forThread returned NULL\n"); + return 0; + } + + return _ZN7android6Looper8pollOnceEiPiS1_PPv(looper, timeoutMillis, outFd, outEvents, outData); +} + +int _ZN7android6Looper5addFdEiiiPFiiiPvES1_(ALooper *this, int fd, int ident, int events, Looper_callbackFunc callback, void *data); int ALooper_addFd(ALooper* looper, int fd, int ident, int events, Looper_callbackFunc callback, void* data) { return _ZN7android6Looper5addFdEiiiPFiiiPvES1_(looper, fd, ident, events, callback, data); } + void _ZN7android6Looper4wakeEv(ALooper *this); -void ALooper_wake(ALooper* looper) { - _ZN7android6Looper4wakeEv(looper); +void ALooper_wake(ALooper *looper) +{ + _ZN7android6Looper4wakeEv(looper); +} + +/* this is not part of the android API, but we use it internally */ + +bool _ZNK7android6Looper9isPollingEv(ALooper *this); +bool ALooper_isPolling(ALooper *looper) +{ + return _ZNK7android6Looper9isPollingEv(looper); }