make Looper, Handler, and MessageQueue work properly

this for example makes Unity apps not steal the main thread,
hanging Gtk.
This commit is contained in:
Mis012 2023-07-25 14:26:29 +02:00
parent 7ac5587fca
commit 08998b0076
15 changed files with 997 additions and 158 deletions

View file

@ -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',

View file

@ -0,0 +1,62 @@
#include "defines.h"
#include "util.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
/* 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);
}

View file

@ -1,3 +1,6 @@
#include <time.h>
#include <math.h>
#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);
}

View file

@ -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
}

View file

@ -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

View file

@ -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) {}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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) {

View file

@ -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.
* <p>
* Causes the handler thread's looper to terminate without processing any
* more messages in the message queue.
* </p><p>
* 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.
* </p><p class="note">
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p><p>
* 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.
* </p><p>
* 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.
* </p>
*
* @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;
}
}

View file

@ -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<Looper> sThreadLocal = new ThreadLocal<Looper>();
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;
}
/**

View file

@ -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<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
@ -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

View file

@ -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.

View file

@ -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.
* <p>
* 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.
* </p><p>
* 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.
* </p>
* <ul>
* <li>To post an animation to be processed on a regular time basis synchronized with
* display frame rendering, use {@link android.animation.ValueAnimator#start}.</li>
* <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
* frame, use {@link View#postOnAnimation}.</li>
* <li>To post a {@link Runnable} to be invoked once at the beginning of the next display
* frame after a delay, use {@link View#postOnAnimationDelayed}.</li>
* <li>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)}.</li>
* <li>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.</li>
* </ul>
* <p>
* 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.
* </p>
* <ul>
* <li>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}.</li>
* <li>... and that's about it.</li>
* </ul>
* <p>
* 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.
* </p>
*/
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<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@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.
* <p>
* 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.
* </p><p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p><p>
* 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.
* </p>
*
* @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.
* <p>
* 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.
* </p><p>
* 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.
* </p>
*
* @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.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @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.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @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.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @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.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @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.
* <p>
* 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.
* </p><p>
* 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.
* </p><p>
* This method should only be called from within a callback.
* </p>
*
* @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.
* <p>
* 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.
* </p><p>
* 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.
* </p>
*
* @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;
}
}
}
}

View file

@ -1,3 +1,4 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
@ -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);
}