Implement WebView via Playwright (#1434)

* Implement Android's Looper

Looper handles thread messaging. This is used by extensions when they
want to enqueue actions e.g. for sleeping while WebView does someting

* Stub WebView

* Continue stubbing ViewGroup for WebView

* Implement WebView via Playwright

* Lint

* Implement request interception

Supports Yidan

* Support WebChromeClient

For Bokugen

* Fix onPageStarted

* Make Playwright configurable

* Subscribe to config changes

* Fix exposing of functions

* Support data urls

* Looper: Fix infinite sleep

* Looper: Avoid killing the loop on exception

Just log it and continue

* Pump playwright's message queue periodically

https://playwright.dev/java/docs/multithreading#pagewaitfortimeout-vs-threadsleep

* Update server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Stub a KCef WebViewProvider

* Initial Kcef Webview implementation

Still buggy, on the second call it just seems to fall over

* Format, restructure to create browser on load

This is much more consistent, before we would sometimes see errors from
about:blank, which block the actual page

* Implement some small useful properties

* Move inline objects to class

* Handle requests in Kcef

* Move Playwright implementation

* Document Playwright settings, fix deprecated warnings

* Inject default user agent from NetworkHelper

* Move playwright to libs.versions.toml

* Lint

* Fix missing imports after lint

* Update server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Fix default user agent set/get

Use System.getProperty instead of SystemProperties.get

* Configurable WebView provider implementation

* Simplify Playwright settings init

* Minor cleanup and improvements

* Remove playwright WebView impl

* Document WebView for Linux

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-06-12 17:38:54 +02:00
committed by GitHub
parent dee61e191c
commit a2fadbe513
30 changed files with 18694 additions and 4 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
/* //device/java/android/android/app/IActivityPendingResult.aidl
**
** Copyright 2007, 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;
import android.os.Message;
/** @hide */
/* oneway */ interface IMessenger {
void send(/* in */ Message msg);
}
@@ -0,0 +1,575 @@
/*
* 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Log;
import android.util.Printer;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import java.util.Objects;
/**
* Class used to run a message loop for a thread. Threads by default do
* not have a message loop associated with them; to create one, call
* {@link #prepare} in the thread that is to run the loop, and then
* {@link #loop} to have it process messages until the loop is stopped.
*
* <p>Most interaction with a message loop is through the
* {@link Handler} class.
*
* <p>This is a typical example of the implementation of a Looper thread,
* using the separation of {@link #prepare} and {@link #loop} to create an
* initial Handler to communicate with the Looper.
*
* <pre>
* class LooperThread extends Thread {
* public Handler mHandler;
*
* public void run() {
* Looper.prepare();
*
* mHandler = new Handler(Looper.myLooper()) {
* public void handleMessage(Message msg) {
* // process incoming messages here
* }
* };
*
* Looper.loop();
* }
* }</pre>
*/
public final class Looper {
/*
* API Implementation Note:
*
* This class contains the code required to set up and manage an event loop
* based on MessageQueue. APIs that affect the state of the queue should be
* defined on MessageQueue or Handler rather than on Looper itself. For example,
* idle handlers and sync barriers are defined on the queue whereas preparing the
* thread, looping, and quitting are defined on the looper.
*/
private static final String TAG = "Looper";
private static class NoImagePreloadHolder {
// Enable/Disable verbose logging with a system prop. e.g.
// adb shell 'setprop log.looper.slow.verbose false && stop && start'
private static final boolean sVerboseLogging =
SystemProperties.getBoolean("log.looper.slow.verbose", false);
}
// 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
private static Observer sObserver;
final MessageQueue mQueue;
final Thread mThread;
private boolean mInLoop;
private Printer mLogging;
private long mTraceTag;
/**
* If set, the looper will show a warning log if a message dispatch takes longer than this.
*/
private long mSlowDispatchThresholdMs;
/**
* If set, the looper will show a warning log if a message delivery (actual delivery time -
* post time) takes longer than this.
*/
private long mSlowDeliveryThresholdMs;
/**
* True if a message delivery takes longer than {@link #mSlowDeliveryThresholdMs}.
*/
private boolean mSlowDeliveryDetected;
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. See also: {@link #prepare()}
*
* @deprecated The main looper for your application is created by the Android environment,
* so you should never need to call this function yourself.
*/
@Deprecated
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/**
* Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
/**
* Force the application's main looper to the given value. The main looper is typically
* configured automatically by the OS, so this capability is only intended to enable testing.
*
* @hide
*/
public static void setMainLooperForTest(@NonNull Looper looper) {
synchronized (Looper.class) {
sMainLooper = Objects.requireNonNull(looper);
}
}
/**
* Clear the application's main looper to be undefined. The main looper is typically
* configured automatically by the OS, so this capability is only intended to enable testing.
*
* @hide
*/
public static void clearMainLooperForTest() {
synchronized (Looper.class) {
sMainLooper = null;
}
}
/**
* Set the transaction observer for all Loopers in this process.
*
* @hide
*/
public static void setObserver(@Nullable Observer observer) {
sObserver = observer;
}
/**
* Poll and deliver single message, return true if the outer loop should continue.
*/
@SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
"ClearIdentityCallNotFollowedByTryFinally"})
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
final boolean hasOverride = thresholdOverride >= 0;
if (hasOverride) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride)
&& (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
Log.e(TAG, "Loop handler threw", exception);
// throw exception;
} finally {
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
ThreadLocalWorkSource.restore(origWorkSource);
}
if (logSlowDelivery) {
boolean slow = false;
if (!me.mSlowDeliveryDetected || NoImagePreloadHolder.sVerboseLogging) {
slow = showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart,
"delivery", msg);
}
if (me.mSlowDeliveryDetected) {
if (!slow && (dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
me.mSlowDeliveryDetected = false;
}
} else if (slow) {
// A slow delivery is detected, suppressing further logs unless verbose logging
// is enabled.
me.mSlowDeliveryDetected = true;
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
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.recycleUnchecked();
return true;
}
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
@SuppressWarnings({"UnusedTokenOfOriginalCallingIdentity",
"ClearIdentityCallNotFollowedByTryFinally",
"ResultOfClearIdentityCallNotStoredInVariable"})
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
if (me.mInLoop) {
Slog.w(TAG, "Loop again would have the queued messages be executed"
+ " before this one completed.");
}
me.mInLoop = true;
// 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();
final long ident = 0;
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride = getThresholdOverride();
me.mSlowDeliveryDetected = false;
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
private static int getThresholdOverride() {
return -1;
// // Allow overriding the threshold for all processes' main looper with a system prop.
// // e.g. adb shell 'setprop log.looper.any.main.slow 1 && stop && start'
// if (myLooper() == getMainLooper()) {
// final int globalOverride = SystemProperties.getInt("log.looper.any.main.slow", -1);
// if (globalOverride >= 0) {
// return globalOverride;
// }
// }
// // Allow overriding the threshold for all threads within a process with a system prop.
// // e.g. adb shell 'setprop log.looper.1000.any.slow 1 && stop && start'
// final int processOverride = SystemProperties.getInt("log.looper."
// + Process.myUid() + ".any.slow", -1);
// if (processOverride >= 0) {
// return processOverride;
// }
// return SystemProperties.getInt("log.looper."
// + Process.myUid() + "."
// + Thread.currentThread().getName()
// + ".slow", -1);
}
private static int getThresholdOverride$ravenwood() {
return -1;
}
private static int getThreadGroup() {
int threadGroup = Process.THREAD_GROUP_DEFAULT;
if (!Process.isIsolated()) {
threadGroup = Process.getProcessGroup(Process.myTid());
}
return threadGroup;
}
private static String threadGroupToString(int threadGroup) {
switch (threadGroup) {
case Process.THREAD_GROUP_SYSTEM:
return "SYSTEM";
case Process.THREAD_GROUP_AUDIO_APP:
return "AUDIO_APP";
case Process.THREAD_GROUP_AUDIO_SYS:
return "AUDIO_SYS";
case Process.THREAD_GROUP_TOP_APP:
return "TOP_APP";
case Process.THREAD_GROUP_RT_APP:
return "RT_APP";
default:
return "UNKNOWN";
}
}
private static boolean showSlowLog(long threshold, long measureStart, long measureEnd,
String what, Message msg) {
final long actualTime = measureEnd - measureStart;
if (actualTime < threshold) {
return false;
}
String name = /* Process.myProcessName() */ "Stub!";
String threadGroup = threadGroupToString(getThreadGroup());
boolean isMain = myLooper() == getMainLooper();
// For slow delivery, the current message isn't really important, but log it anyway.
Slog.w(TAG, "Slow " + what + " took " + actualTime + "ms "
+ Thread.currentThread().getName() + " app=" + name
+ " main=" + isMain + " group=" + threadGroup
+ " h=" + msg.target.getClass().getName() + " c=" + msg.callback
+ " m=" + msg.what);
return true;
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
/**
* Return the {@link MessageQueue} object associated with the current
* thread. This must be called from a thread running a Looper, or a
* NullPointerException will be thrown.
*/
public static @NonNull MessageQueue myQueue() {
return myLooper().mQueue;
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
/**
* Returns true if the current thread is this looper's thread.
*/
public boolean isCurrentThread() {
return Thread.currentThread() == mThread;
}
/**
* Control logging of messages as they are processed by this Looper. If
* enabled, a log message will be written to <var>printer</var>
* at the beginning and ending of each message dispatch, identifying the
* target Handler and message contents.
*
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
*/
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
/** {@hide} */
public void setTraceTag(long traceTag) {
mTraceTag = traceTag;
}
/**
* Set a thresholds for slow dispatch/delivery log.
* {@hide}
*/
public void setSlowLogThresholdMs(long slowDispatchThresholdMs, long slowDeliveryThresholdMs) {
mSlowDispatchThresholdMs = slowDispatchThresholdMs;
mSlowDeliveryThresholdMs = slowDeliveryThresholdMs;
}
/**
* Quits the looper.
* <p>
* Causes the {@link #loop} method 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>
*
* @see #quitSafely
*/
public void quit() {
mQueue.quit(false);
}
/**
* Quits the looper safely.
* <p>
* Causes the {@link #loop} method to terminate as soon as all remaining messages
* in the message queue that are already due to be delivered have been handled.
* However pending delayed messages with due times in the future will not be
* delivered before the loop terminates.
* </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>
*/
public void quitSafely() {
mQueue.quit(true);
}
/**
* Gets the Thread associated with this Looper.
*
* @return The looper's thread.
*/
public @NonNull Thread getThread() {
return mThread;
}
/**
* Gets this looper's message queue.
*
* @return The looper's message queue.
*/
public @NonNull MessageQueue getQueue() {
return mQueue;
}
/**
* Dumps the state of the looper for debugging purposes.
*
* @param pw A printer to receive the contents of the dump.
* @param prefix A prefix to prepend to each line which is printed.
*/
public void dump(@NonNull Printer pw, @NonNull String prefix) {
throw new RuntimeException("Stub!");
}
/**
* Dumps the state of the looper for debugging purposes.
*
* @param pw A printer to receive the contents of the dump.
* @param prefix A prefix to prepend to each line which is printed.
* @param handler Only dump messages for this Handler.
* @hide
*/
public void dump(@NonNull Printer pw, @NonNull String prefix, Handler handler) {
throw new RuntimeException("Stub!");
}
/** @hide */
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
throw new RuntimeException("Stub!");
}
@Override
public String toString() {
return "Looper (" + mThread.getName() + ", tid " + mThread.getId()
+ ") {" + Integer.toHexString(System.identityHashCode(this)) + "}";
}
/** {@hide} */
public interface Observer {
/**
* Called right before a message is dispatched.
*
* <p> The token type is not specified to allow the implementation to specify its own type.
*
* @return a token used for collecting telemetry when dispatching a single message.
* The token token must be passed back exactly once to either
* {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}
* and must not be reused again.
*
*/
Object messageDispatchStarting();
/**
* Called when a message was processed by a Handler.
*
* @param token Token obtained by previously calling
* {@link Observer#messageDispatchStarting} on the same Observer instance.
* @param msg The message that was dispatched.
*/
void messageDispatched(Object token, Message msg);
/**
* Called when an exception was thrown while processing a message.
*
* @param token Token obtained by previously calling
* {@link Observer#messageDispatchStarting} on the same Observer instance.
* @param msg The message that was dispatched and caused an exception.
* @param exception The exception that was thrown.
*/
void dispatchingThrewException(Object token, Message msg, Exception exception);
}
}
@@ -0,0 +1,630 @@
/*
* 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;
import android.annotation.Nullable;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
/**
*
* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}. This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p class="note">While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>
*/
public final class Message implements Parcelable {
/**
* User-defined message code so that the recipient can identify
* what this message is about. Each {@link Handler} has its own name-space
* for message codes, so you do not need to worry about yours conflicting
* with other handlers.
*
* If not specified, this value is 0.
* Use values other than 0 to indicate custom message codes.
*/
public int what;
/**
* arg1 and arg2 are lower-cost alternatives to using
* {@link #setData(Bundle) setData()} if you only need to store a
* few integer values.
*/
public int arg1;
/**
* arg1 and arg2 are lower-cost alternatives to using
* {@link #setData(Bundle) setData()} if you only need to store a
* few integer values.
*/
public int arg2;
/**
* An arbitrary object to send to the recipient. When using
* {@link Messenger} to send the message across processes this can only
* be non-null if it contains a Parcelable of a framework class (not one
* implemented by the application). For other data transfer use
* {@link #setData}.
*
* <p>Note that Parcelable objects here are not supported prior to
* the {@link android.os.Build.VERSION_CODES#FROYO} release.
*/
public Object obj;
/**
* Optional Messenger where replies to this message can be sent. The
* semantics of exactly how this is used are up to the sender and
* receiver.
*/
public Messenger replyTo;
/**
* Indicates that the uid is not set;
*
* @hide Only for use within the system server.
*/
public static final int UID_NONE = -1;
/**
* Optional field indicating the uid that sent the message. This is
* only valid for messages posted by a {@link Messenger}; otherwise,
* it will be -1.
*/
public int sendingUid = UID_NONE;
/**
* Optional field indicating the uid that caused this message to be enqueued.
*
* @hide Only for use within the system server.
*/
public int workSourceUid = UID_NONE;
/** If set message is in use.
* This flag is set when the message is enqueued and remains set while it
* is delivered and afterwards when it is recycled. The flag is only cleared
* when a new message is created or obtained since that is the only time that
* applications are allowed to modify the contents of the message.
*
* It is an error to attempt to enqueue or recycle a message that is already in use.
*/
/*package*/ static final int FLAG_IN_USE = 1 << 0;
/** If set message is asynchronous */
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;
/** Flags to clear in the copyFrom method */
/*package*/ static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAG_IN_USE;
/*package*/ int flags;
/**
* The targeted delivery time of this message. The time-base is
* {@link SystemClock#uptimeMillis}.
* @hide Only for use within the tests.
*/
public long when;
/** @hide */
@SuppressWarnings("unused")
public long mInsertSeq;
/*package*/ Bundle data;
/*package*/ Handler target;
/*package*/ Runnable callback;
// sometimes we store linked lists of these things
/*package*/ Message next;
/** @hide */
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
private static boolean gCheckRecycle = true;
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
/**
* Same as {@link #obtain()}, but copies the values of an existing
* message (including its target) into the new one.
* @param orig Original message to copy.
* @return A Message object from the global pool.
*/
public static Message obtain(Message orig) {
Message m = obtain();
m.what = orig.what;
m.arg1 = orig.arg1;
m.arg2 = orig.arg2;
m.obj = orig.obj;
m.replyTo = orig.replyTo;
m.sendingUid = orig.sendingUid;
m.workSourceUid = orig.workSourceUid;
if (orig.data != null) {
m.data = new Bundle(orig.data);
}
m.target = orig.target;
m.callback = orig.callback;
return m;
}
/**
* Same as {@link #obtain()}, but sets the value for the <em>target</em> member on the Message returned.
* @param h Handler to assign to the returned Message object's <em>target</em> member.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
/**
* Same as {@link #obtain(Handler)}, but assigns a callback Runnable on
* the Message that is returned.
* @param h Handler to assign to the returned Message object's <em>target</em> member.
* @param callback Runnable that will execute when the message is handled.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h, Runnable callback) {
Message m = obtain();
m.target = h;
m.callback = callback;
return m;
}
/**
* Same as {@link #obtain()}, but sets the values for both <em>target</em> and
* <em>what</em> members on the Message.
* @param h Value to assign to the <em>target</em> member.
* @param what Value to assign to the <em>what</em> member.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h, int what) {
Message m = obtain();
m.target = h;
m.what = what;
return m;
}
/**
* Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>, and <em>obj</em>
* members.
* @param h The <em>target</em> value to set.
* @param what The <em>what</em> value to set.
* @param obj The <em>object</em> method to set.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h, int what, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.obj = obj;
return m;
}
/**
* Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>,
* <em>arg1</em>, and <em>arg2</em> members.
*
* @param h The <em>target</em> value to set.
* @param what The <em>what</em> value to set.
* @param arg1 The <em>arg1</em> value to set.
* @param arg2 The <em>arg2</em> value to set.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h, int what, int arg1, int arg2) {
Message m = obtain();
m.target = h;
m.what = what;
m.arg1 = arg1;
m.arg2 = arg2;
return m;
}
/**
* Same as {@link #obtain()}, but sets the values of the <em>target</em>, <em>what</em>,
* <em>arg1</em>, <em>arg2</em>, and <em>obj</em> members.
*
* @param h The <em>target</em> value to set.
* @param what The <em>what</em> value to set.
* @param arg1 The <em>arg1</em> value to set.
* @param arg2 The <em>arg2</em> value to set.
* @param obj The <em>obj</em> value to set.
* @return A Message object from the global pool.
*/
public static Message obtain(Handler h, int what,
int arg1, int arg2, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.arg1 = arg1;
m.arg2 = arg2;
m.obj = obj;
return m;
}
/** @hide */
public static void updateCheckRecycle(int targetSdkVersion) {
if (targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
gCheckRecycle = false;
}
}
/**
* Return a Message instance to the global pool.
* <p>
* You MUST NOT touch the Message after calling this function because it has
* effectively been freed. It is an error to recycle a message that is currently
* enqueued or that is in the process of being delivered to a Handler.
* </p>
*/
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
/**
* Recycles a Message that may be in-use.
* Used internally by the MessageQueue and Looper when disposing of queued Messages.
*/
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
/**
* Make this message like o. Performs a shallow copy of the data field.
* Does not copy the linked list fields, nor the timestamp or
* target/callback of the original message.
*/
public void copyFrom(Message o) {
this.flags = o.flags & ~FLAGS_TO_CLEAR_ON_COPY_FROM;
this.what = o.what;
this.arg1 = o.arg1;
this.arg2 = o.arg2;
this.obj = o.obj;
this.replyTo = o.replyTo;
this.sendingUid = o.sendingUid;
this.workSourceUid = o.workSourceUid;
if (o.data != null) {
this.data = (Bundle) o.data.clone();
} else {
this.data = null;
}
}
/**
* Return the targeted delivery time of this message, in milliseconds.
*/
public long getWhen() {
return when;
}
public void setTarget(Handler target) {
this.target = target;
}
/**
* Retrieve the {@link android.os.Handler Handler} implementation that
* will receive this message. The object must implement
* {@link android.os.Handler#handleMessage(android.os.Message)
* Handler.handleMessage()}. Each Handler has its own name-space for
* message codes, so you do not need to
* worry about yours conflicting with other handlers.
*/
public Handler getTarget() {
return target;
}
/**
* Retrieve callback object that will execute when this message is handled.
* This object must implement Runnable. This is called by
* the <em>target</em> {@link Handler} that is receiving this Message to
* dispatch it. If
* not set, the message will be dispatched to the receiving Handler's
* {@link Handler#handleMessage(Message)}.
*/
public Runnable getCallback() {
return callback;
}
/** @hide */
public Message setCallback(Runnable r) {
callback = r;
return this;
}
/**
* Obtains a Bundle of arbitrary data associated with this
* event, lazily creating it if necessary. Set this value by calling
* {@link #setData(Bundle)}. Note that when transferring data across
* processes via {@link Messenger}, you will need to set your ClassLoader
* on the Bundle via {@link Bundle#setClassLoader(ClassLoader)
* Bundle.setClassLoader()} so that it can instantiate your objects when
* you retrieve them.
* @see #peekData()
* @see #setData(Bundle)
*/
public Bundle getData() {
if (data == null) {
data = new Bundle();
}
return data;
}
/**
* Like getData(), but does not lazily create the Bundle. A null
* is returned if the Bundle does not already exist. See
* {@link #getData} for further information on this.
* @see #getData()
* @see #setData(Bundle)
*/
@Nullable
public Bundle peekData() {
return data;
}
/**
* Sets a Bundle of arbitrary data values. Use arg1 and arg2 members
* as a lower cost way to send a few simple integer values, if you can.
* @see #getData()
* @see #peekData()
*/
public void setData(Bundle data) {
this.data = data;
}
/**
* Chainable setter for {@link #what}
*
* @hide
*/
public Message setWhat(int what) {
this.what = what;
return this;
}
/**
* Sends this Message to the Handler specified by {@link #getTarget}.
* Throws a null pointer exception if this field has not been set.
*/
public void sendToTarget() {
target.sendMessage(this);
}
/**
* Returns true if the message is asynchronous, meaning that it is not
* subject to {@link Looper} synchronization barriers.
*
* @return True if the message is asynchronous.
*
* @see #setAsynchronous(boolean)
*/
public boolean isAsynchronous() {
return (flags & FLAG_ASYNCHRONOUS) != 0;
}
/**
* Sets whether the message is asynchronous, meaning that it is not
* subject to {@link Looper} synchronization barriers.
* <p>
* Certain operations, such as view invalidation, may introduce synchronization
* barriers into the {@link Looper}'s message queue to prevent subsequent messages
* from being delivered until some condition is met. In the case of view invalidation,
* messages which are posted after a call to {@link android.view.View#invalidate}
* are suspended by means of a synchronization barrier until the next frame is
* ready to be drawn. The synchronization barrier ensures that the invalidation
* request is completely handled before resuming.
* </p><p>
* Asynchronous messages are exempt from synchronization barriers. They typically
* represent interrupts, input events, and other signals that must be handled independently
* even while other work has been suspended.
* </p><p>
* Note that asynchronous messages may be delivered out of order with respect to
* synchronous messages although they are always delivered in order among themselves.
* If the relative order of these messages matters then they probably should not be
* asynchronous in the first place. Use with caution.
* </p>
*
* @param async True if the message is asynchronous.
*
* @see #isAsynchronous()
*/
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
/*package*/ boolean isInUse() {
return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
}
/*package*/ void markInUse() {
flags |= FLAG_IN_USE;
}
/** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
*/
public Message() {
}
@Override
public String toString() {
return toString(SystemClock.uptimeMillis());
}
String toString(long now) {
StringBuilder b = new StringBuilder();
b.append("{ when=");
TimeUtils.formatDuration(when - now, b);
if (target != null) {
if (callback != null) {
b.append(" callback=");
b.append(callback.getClass().getName());
} else {
b.append(" what=");
b.append(what);
}
if (arg1 != 0) {
b.append(" arg1=");
b.append(arg1);
}
if (arg2 != 0) {
b.append(" arg2=");
b.append(arg2);
}
if (obj != null) {
b.append(" obj=");
b.append(obj);
}
b.append(" target=");
b.append(target.getClass().getName());
} else {
b.append(" barrier=");
b.append(arg1);
}
b.append(" }");
return b.toString();
}
public static final @android.annotation.NonNull Parcelable.Creator<Message> CREATOR
= new Parcelable.Creator<Message>() {
public Message createFromParcel(Parcel source) {
Message msg = Message.obtain();
msg.readFromParcel(source);
return msg;
}
public Message[] newArray(int size) {
return new Message[size];
}
};
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
if (callback != null) {
throw new RuntimeException(
"Can't marshal callbacks across processes.");
}
dest.writeInt(what);
dest.writeInt(arg1);
dest.writeInt(arg2);
if (obj != null) {
try {
Parcelable p = (Parcelable)obj;
dest.writeInt(1);
dest.writeParcelable(p, flags);
} catch (ClassCastException e) {
throw new RuntimeException(
"Can't marshal non-Parcelable objects across processes.");
}
} else {
dest.writeInt(0);
}
dest.writeLong(when);
dest.writeBundle(data);
Messenger.writeMessengerOrNullToParcel(replyTo, dest);
dest.writeInt(sendingUid);
dest.writeInt(workSourceUid);
}
private void readFromParcel(Parcel source) {
what = source.readInt();
arg1 = source.readInt();
arg2 = source.readInt();
if (source.readInt() != 0) {
obj = source.readParcelable(getClass().getClassLoader());
}
when = source.readLong();
data = source.readBundle();
replyTo = Messenger.readMessengerOrNullFromParcel(source);
sendingUid = source.readInt();
workSourceUid = source.readInt();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2018 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;
/**
* Tracks who triggered the work currently executed on this thread.
*
* <p>ThreadLocalWorkSource is automatically updated inside system server for incoming/outgoing
* binder calls and messages posted to handler threads.
*
* <p>ThreadLocalWorkSource can also be set manually if needed to refine the WorkSource.
*
* <p>Example:
* <ul>
* <li>Bluetooth process calls {@link PowerManager#isInteractive()} API on behalf of app foo.
* <li>ThreadLocalWorkSource will be automatically set to the UID of foo.
* <li>Any code on the thread handling {@link PowerManagerService#isInteractive()} can call
* {@link ThreadLocalWorkSource#getUid()} to blame any resource used to handle this call.
* <li>If a message is posted from the binder thread, the code handling the message can also call
* {@link ThreadLocalWorkSource#getUid()} and it will return the UID of foo since the work source is
* automatically propagated.
* </ul>
*
* @hide Only for use within system server.
*/
public final class ThreadLocalWorkSource {
public static final int UID_NONE = Message.UID_NONE;
private static final ThreadLocal<int []> sWorkSourceUid =
ThreadLocal.withInitial(() -> new int[] {UID_NONE});
/**
* Returns the UID to blame for the code currently executed on this thread.
*
* <p>This UID is set automatically by common frameworks (e.g. Binder and Handler frameworks)
* and automatically propagated inside system server.
* <p>It can also be set manually using {@link #setUid(int)}.
*/
public static int getUid() {
return sWorkSourceUid.get()[0];
}
/**
* Sets the UID to blame for the code currently executed on this thread.
*
* <p>Inside system server, this UID will be automatically propagated.
* <p>It will be used to attribute future resources used on this thread (e.g. binder
* transactions or processing handler messages) and on any other threads the UID is propagated
* to.
*
* @return a token that can be used to restore the state.
*/
public static long setUid(int uid) {
final long token = getToken();
sWorkSourceUid.get()[0] = uid;
return token;
}
/**
* Restores the state using the provided token.
*/
public static void restore(long token) {
sWorkSourceUid.get()[0] = parseUidFromToken(token);
}
/**
* Clears the stored work source uid.
*
* <p>This method should be used when we do not know who to blame. If the UID to blame is the
* UID of the current process, it is better to attribute the work to the current process
* explicitly instead of clearing the work source:
*
* <pre>
* ThreadLocalWorkSource.setUid(Process.myUid());
* </pre>
*
* @return a token that can be used to restore the state.
*/
public static long clear() {
return setUid(UID_NONE);
}
private static int parseUidFromToken(long token) {
return (int) token;
}
private static long getToken() {
return sWorkSourceUid.get()[0];
}
private ThreadLocalWorkSource() {
}
}
@@ -0,0 +1,168 @@
package android.os.shadows;
// package org.robolectric.res.android;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A unique id per object registry. Used to emulate android platform behavior of storing a long
* which represents a pointer to an object.
*/
public class NativeObjRegistry<T> {
private static final int INITIAL_ID = 1;
private final String name;
private final boolean debug;
private final HashMap<Long, T> nativeObjToIdMap = new HashMap<Long, T>();
private final Map<Long, DebugInfo> idToDebugInfoMap;
private long nextId = INITIAL_ID;
public NativeObjRegistry(Class<T> theClass) {
this(theClass, false);
}
public NativeObjRegistry(Class<T> theClass, boolean debug) {
this(theClass.getSimpleName(), debug);
}
public NativeObjRegistry(String name) {
this(name, false);
}
public NativeObjRegistry(String name, boolean debug) {
this.name = name;
this.debug = debug;
this.idToDebugInfoMap = debug ? new HashMap<>() : null;
}
private Long getNativeObjectId(T o) {
for(Map.Entry<Long, T> entry : nativeObjToIdMap.entrySet()) {
if (o == entry.getValue())
return entry.getKey();
}
return null;
}
/**
* Register and assign a new unique native id for given object (representing a C memory pointer).
*
* @throws IllegalStateException if the object was previously registered
*/
public synchronized long register(T o) {
if (o == null)
throw new IllegalStateException("Object must not be null");
Long nativeId = getNativeObjectId(o);
if (nativeId != null) {
if (debug) {
DebugInfo debugInfo = idToDebugInfoMap.get(nativeId);
if (debugInfo != null) {
System.out.printf(
"NativeObjRegistry %s: register %d -> %s already registered:%n", name, nativeId, o);
debugInfo.registrationTrace.printStackTrace(System.out);
}
}
throw new IllegalStateException("Object was previously registered with id " + nativeId);
}
nativeId = nextId;
if (debug) {
System.out.printf("NativeObjRegistry %s: register %d -> %s%n", name, nativeId, o);
idToDebugInfoMap.put(nativeId, new DebugInfo(new Trace()));
}
nativeObjToIdMap.put(nativeId, o);
nextId++;
return nativeId;
}
/**
* Unregister an object previously registered with {@link #register(Object)}.
*
* @param nativeId the unique id (representing a C memory pointer) of the object to unregister.
* @throws IllegalStateException if the object was never registered, or was previously
* unregistered.
*/
public synchronized T unregister(long nativeId) {
T o = nativeObjToIdMap.remove(nativeId);
if (debug) {
System.out.printf("NativeObjRegistry %s: unregister %d -> %s%n", name, nativeId, o);
new RuntimeException("unregister debug").printStackTrace(System.out);
}
if (o == null) {
if (debug) {
DebugInfo debugInfo = idToDebugInfoMap.get(nativeId);
debugInfo.unregistrationTraces.add(new Trace());
if (debugInfo.unregistrationTraces.size() > 1) {
System.out.format("NativeObjRegistry %s: Too many unregistrations:%n", name);
for (Trace unregistration : debugInfo.unregistrationTraces) {
unregistration.printStackTrace(System.out);
}
}
}
throw new IllegalStateException(
nativeId + " has already been removed (or was never registered)");
}
return o;
}
/** Retrieve the native object for given id. Throws if object with that id cannot be found */
public synchronized T getNativeObject(long nativeId) {
T object = nativeObjToIdMap.get(nativeId);
if (object != null) {
return object;
} else {
throw new NullPointerException(
String.format(
"Could not find object with nativeId: %d. Currently registered ids: %s",
nativeId, nativeObjToIdMap.keySet()));
}
}
/**
* Updates the native object for the given id.
*
* @throws IllegalStateException if no object was registered with the given id before
*/
public synchronized void update(long nativeId, T o) {
T previous = nativeObjToIdMap.get(nativeId);
if (previous == null) {
throw new IllegalStateException("Native id " + nativeId + " was never registered");
}
if (debug) {
System.out.printf("NativeObjRegistry %s: update %d -> %s%n", name, nativeId, o);
idToDebugInfoMap.put(nativeId, new DebugInfo(new Trace()));
}
nativeObjToIdMap.put(nativeId, o);
}
/**
* Similar to {@link #getNativeObject(long)} but returns null if object with given id cannot be
* found.
*/
public synchronized T peekNativeObject(long nativeId) {
return nativeObjToIdMap.get(nativeId);
}
/** WARNING -- dangerous! Call {@link #unregister(long)} instead! */
public synchronized void clear() {
nextId = INITIAL_ID;
nativeObjToIdMap.clear();
}
private static class DebugInfo {
final Trace registrationTrace;
final List<Trace> unregistrationTraces = new ArrayList<>();
public DebugInfo(Trace trace) {
registrationTrace = trace;
}
}
private static class Trace extends Throwable {
private Trace() {}
}
}
@@ -0,0 +1,73 @@
package android.os.shadows;
// package org.robolectric.shadows;
// and badly gutted
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.MessageQueue.IdleHandler;
import android.os.SystemClock;
import android.util.Log;
import java.time.Duration;
import java.util.ArrayList;
/**
* The shadow {@link} MessageQueue} for {@link LooperMode.Mode.PAUSED}
*
* <p>This class should not be referenced directly. Use {@link ShadowMessageQueue} instead.
*/
@SuppressWarnings("SynchronizeOnNonFinalField")
public class ShadowPausedMessageQueue {
// just use this class as the native object
private static NativeObjRegistry<ShadowPausedMessageQueue> nativeQueueRegistry =
new NativeObjRegistry<ShadowPausedMessageQueue>(ShadowPausedMessageQueue.class);
private boolean isPolling = false;
private Exception uncaughtException = null;
// shadow constructor instead of nativeInit because nativeInit signature has changed across SDK
// versions
public static long nativeInit() {
return nativeQueueRegistry.register(new ShadowPausedMessageQueue());
}
public static void nativeDestroy(long ptr) {
nativeQueueRegistry.unregister(ptr);
}
public static void nativePollOnce(long ptr, int timeoutMillis) {
ShadowPausedMessageQueue obj = nativeQueueRegistry.getNativeObject(ptr);
obj.nativePollOnce(timeoutMillis);
}
public void nativePollOnce(int timeoutMillis) {
if (timeoutMillis == 0) {
return;
}
synchronized (this) {
isPolling = true;
try {
if (timeoutMillis < 0) {
this.wait();
} else {
this.wait(timeoutMillis);
}
} catch (InterruptedException e) {
// ignore
}
isPolling = false;
}
}
public static void nativeWake(long ptr) {
ShadowPausedMessageQueue obj = nativeQueueRegistry.getNativeObject(ptr);
synchronized (obj) {
obj.notifyAll();
}
}
public static boolean nativeIsPolling(long ptr) {
return nativeQueueRegistry.getNativeObject(ptr).isPolling;
}
}