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

View File

@@ -136,4 +136,4 @@ public final class Log {
first.append(msg);
return first.toString();
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* 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.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Build;
/**
* API for sending log output to the {@link Log#LOG_ID_SYSTEM} buffer.
*
* <p>Should be used by system components. Use {@code adb logcat --buffer=system} to fetch the logs.
*
* @see Log
* @hide
*/
public final class Slog {
private Slog() {
}
/**
* Logs {@code msg} at {@link Log#VERBOSE} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#v(String, String)
*/
public static int v(@Nullable String tag, @NonNull String msg) {
return Log.println(Log.VERBOSE, tag, msg);
}
/**
* Logs {@code msg} at {@link Log#VERBOSE} level, attaching stack trace of the {@code tr} to
* the end of the log statement.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#v(String, String, Throwable)
*/
public static int v(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.println(Log.VERBOSE, tag,
msg + '\n' + Log.getStackTraceString(tr));
}
/**
* Logs {@code msg} at {@link Log#DEBUG} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#d(String, String)
*/
public static int d(@Nullable String tag, @NonNull String msg) {
return Log.println(Log.DEBUG, tag, msg);
}
/**
* Logs {@code msg} at {@link Log#DEBUG} level, attaching stack trace of the {@code tr} to
* the end of the log statement.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#d(String, String, Throwable)
*/
public static int d(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.println(Log.DEBUG, tag,
msg + '\n' + Log.getStackTraceString(tr));
}
/**
* Logs {@code msg} at {@link Log#INFO} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#i(String, String)
*/
public static int i(@Nullable String tag, @NonNull String msg) {
return Log.println(Log.INFO, tag, msg);
}
/**
* Logs {@code msg} at {@link Log#INFO} level, attaching stack trace of the {@code tr} to
* the end of the log statement.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#i(String, String, Throwable)
*/
public static int i(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.println(Log.INFO, tag,
msg + '\n' + Log.getStackTraceString(tr));
}
/**
* Logs {@code msg} at {@link Log#WARN} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#w(String, String)
*/
public static int w(@Nullable String tag, @NonNull String msg) {
return Log.println(Log.WARN, tag, msg);
}
/**
* Logs {@code msg} at {@link Log#WARN} level, attaching stack trace of the {@code tr} to
* the end of the log statement.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#w(String, String, Throwable)
*/
public static int w(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.println(Log.WARN, tag,
msg + '\n' + Log.getStackTraceString(tr));
}
/**
* Logs stack trace of {@code tr} at {@link Log#WARN} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param tr an exception to log.
*
* @see Log#w(String, Throwable)
*/
public static int w(@Nullable String tag, @Nullable Throwable tr) {
return Log.println(Log.WARN, tag, Log.getStackTraceString(tr));
}
/**
* Logs {@code msg} at {@link Log#ERROR} level.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#e(String, String)
*/
public static int e(@Nullable String tag, @NonNull String msg) {
return Log.println(Log.ERROR, tag, msg);
}
/**
* Logs {@code msg} at {@link Log#ERROR} level, attaching stack trace of the {@code tr} to
* the end of the log statement.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#e(String, String, Throwable)
*/
public static int e(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.println(Log.ERROR, tag,
msg + '\n' + Log.getStackTraceString(tr));
}
/**
* Logs a condition that should never happen.
*
* <p>
* Similar to {@link Log#wtf(String, String)}, but will never cause the caller to crash, and
* will always be handled asynchronously. Primarily to be used by the system server.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#wtf(String, String)
*/
public static int wtf(@Nullable String tag, @NonNull String msg) {
return Log.wtf(tag, msg, null);
}
/**
* Logs a condition that should never happen, attaching the full call stack to the log.
*
* <p>
* Similar to {@link Log#wtfStack(String, String)}, but will never cause the caller to crash,
* and will always be handled asynchronously. Primarily to be used by the system server.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
*
* @see Log#wtfStack(String, String)
*/
public static int wtfStack(@Nullable String tag, @NonNull String msg) {
return Log.wtf(tag, msg, null);
}
/**
* Logs a condition that should never happen, attaching stack trace of the {@code tr} to the
* end of the log statement.
*
* <p>
* Similar to {@link Log#wtf(String, Throwable)}, but will never cause the caller to crash,
* and will always be handled asynchronously. Primarily to be used by the system server.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param tr an exception to log.
*
* @see Log#wtf(String, Throwable)
*/
public static int wtf(@Nullable String tag, @Nullable Throwable tr) {
return Log.wtf(tag, tr.getMessage(), tr);
}
/**
* Logs a condition that should never happen, attaching stack trace of the {@code tr} to the
* end of the log statement.
*
* <p>
* Similar to {@link Log#wtf(String, String, Throwable)}, but will never cause the caller to
* crash, and will always be handled asynchronously. Primarily to be used by the system server.
*
* @param tag identifies the source of a log message. It usually represents system service,
* e.g. {@code PackageManager}.
* @param msg the message to log.
* @param tr an exception to log.
*
* @see Log#wtf(String, String, Throwable)
*/
public static int wtf(@Nullable String tag, @NonNull String msg, @Nullable Throwable tr) {
return Log.wtf(tag, msg, tr);
}
/** @hide */
public static int println(int priority, @Nullable String tag, @NonNull String msg) {
return Log.println(priority, tag, msg);
}
}

View File

@@ -0,0 +1,371 @@
/*
* 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.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.os.Build;
import android.os.SystemClock;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* A class containing utility methods related to time zones.
*/
public class TimeUtils {
/** @hide */ public TimeUtils() {}
/** {@hide} */
private static final SimpleDateFormat sLoggingFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/** @hide */
public static final SimpleDateFormat sDumpDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
/**
* This timestamp is used in TimeUtils methods and by the SettingsUI to filter time zones
* to only "effective" ones in a country. It is compared against the notUsedAfter metadata that
* Android records for some time zones.
*
* <p>What is notUsedAfter?</p>
* Android chooses to avoid making users choose between functionally identical time zones at the
* expense of not being able to represent local times in the past.
*
* notUsedAfter exists because some time zones can "merge" with other time zones after a given
* point in time (i.e. they change to have identical transitions, offsets, display names, etc.).
* From the notUsedAfter time, the zone will express the same local time as the one it merged
* with.
*
* <p>Why hardcoded?</p>
* Rather than using System.currentTimeMillis(), a timestamp known to be in the recent past is
* used to ensure consistent behavior across devices and time, and avoid assumptions that the
* system clock on a device is currently set correctly. The fixed value should be updated
* occasionally, but it doesn't have to be very often as effective time zones for a country
* don't change very often.
*
* @hide
*/
public static final Instant MIN_USE_DATE_OF_TIMEZONE =
Instant.ofEpochMilli(1546300800000L); // 1/1/2019 00:00 UTC
/** @hide Field length that can hold 999 days of time */
public static final int HUNDRED_DAY_FIELD_LEN = 19;
private static final int SECONDS_PER_MINUTE = 60;
private static final int SECONDS_PER_HOUR = 60 * 60;
private static final int SECONDS_PER_DAY = 24 * 60 * 60;
/** @hide */
public static final long NANOS_PER_MS = 1000000;
private static final Object sFormatSync = new Object();
private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10];
private static char[] sTmpFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10];
static private int accumField(int amt, int suffix, boolean always, int zeropad) {
if (amt > 999) {
int num = 0;
while (amt != 0) {
num++;
amt /= 10;
}
return num + suffix;
} else {
if (amt > 99 || (always && zeropad >= 3)) {
return 3+suffix;
}
if (amt > 9 || (always && zeropad >= 2)) {
return 2+suffix;
}
if (always || amt > 0) {
return 1+suffix;
}
}
return 0;
}
static private int printFieldLocked(char[] formatStr, int amt, char suffix, int pos,
boolean always, int zeropad) {
if (always || amt > 0) {
final int startPos = pos;
if (amt > 999) {
int tmp = 0;
while (amt != 0 && tmp < sTmpFormatStr.length) {
int dig = amt % 10;
sTmpFormatStr[tmp] = (char)(dig + '0');
tmp++;
amt /= 10;
}
tmp--;
while (tmp >= 0) {
formatStr[pos] = sTmpFormatStr[tmp];
pos++;
tmp--;
}
} else {
if ((always && zeropad >= 3) || amt > 99) {
int dig = amt/100;
formatStr[pos] = (char)(dig + '0');
pos++;
amt -= (dig*100);
}
if ((always && zeropad >= 2) || amt > 9 || startPos != pos) {
int dig = amt/10;
formatStr[pos] = (char)(dig + '0');
pos++;
amt -= (dig*10);
}
formatStr[pos] = (char)(amt + '0');
pos++;
}
formatStr[pos] = suffix;
pos++;
}
return pos;
}
private static int formatDurationLocked(long duration, int fieldLen) {
if (sFormatStr.length < fieldLen) {
sFormatStr = new char[fieldLen];
}
char[] formatStr = sFormatStr;
if (duration == 0) {
int pos = 0;
fieldLen -= 1;
while (pos < fieldLen) {
formatStr[pos++] = ' ';
}
formatStr[pos] = '0';
return pos+1;
}
char prefix;
if (duration > 0) {
prefix = '+';
} else {
prefix = '-';
duration = -duration;
}
int millis = (int)(duration%1000);
int seconds = (int) Math.floor(duration / 1000);
int days = 0, hours = 0, minutes = 0;
if (seconds >= SECONDS_PER_DAY) {
days = seconds / SECONDS_PER_DAY;
seconds -= days * SECONDS_PER_DAY;
}
if (seconds >= SECONDS_PER_HOUR) {
hours = seconds / SECONDS_PER_HOUR;
seconds -= hours * SECONDS_PER_HOUR;
}
if (seconds >= SECONDS_PER_MINUTE) {
minutes = seconds / SECONDS_PER_MINUTE;
seconds -= minutes * SECONDS_PER_MINUTE;
}
int pos = 0;
if (fieldLen != 0) {
int myLen = accumField(days, 1, false, 0);
myLen += accumField(hours, 1, myLen > 0, 2);
myLen += accumField(minutes, 1, myLen > 0, 2);
myLen += accumField(seconds, 1, myLen > 0, 2);
myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1;
while (myLen < fieldLen) {
formatStr[pos] = ' ';
pos++;
myLen++;
}
}
formatStr[pos] = prefix;
pos++;
int start = pos;
boolean zeropad = fieldLen != 0;
pos = printFieldLocked(formatStr, days, 'd', pos, false, 0);
pos = printFieldLocked(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0);
pos = printFieldLocked(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0);
pos = printFieldLocked(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0);
pos = printFieldLocked(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0);
formatStr[pos] = 's';
return pos + 1;
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long duration, StringBuilder builder) {
synchronized (sFormatSync) {
int len = formatDurationLocked(duration, 0);
builder.append(sFormatStr, 0, len);
}
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long duration, StringBuilder builder, int fieldLen) {
synchronized (sFormatSync) {
int len = formatDurationLocked(duration, fieldLen);
builder.append(sFormatStr, 0, len);
}
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long duration, PrintWriter pw, int fieldLen) {
synchronized (sFormatSync) {
int len = formatDurationLocked(duration, fieldLen);
pw.print(new String(sFormatStr, 0, len));
}
}
/** @hide Just for debugging; not internationalized. */
@TestApi
public static String formatDuration(long duration) {
synchronized (sFormatSync) {
int len = formatDurationLocked(duration, 0);
return new String(sFormatStr, 0, len);
}
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long duration, PrintWriter pw) {
formatDuration(duration, pw, 0);
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long time, long now, StringBuilder sb) {
if (time == 0) {
sb.append("--");
return;
}
formatDuration(time-now, sb, 0);
}
/** @hide Just for debugging; not internationalized. */
public static void formatDuration(long time, long now, PrintWriter pw) {
if (time == 0) {
pw.print("--");
return;
}
formatDuration(time-now, pw, 0);
}
/** @hide Just for debugging; not internationalized. */
public static String formatUptime(long time) {
return formatTime(time, SystemClock.uptimeMillis());
}
/** @hide Just for debugging; not internationalized. */
public static String formatRealtime(long time) {
return formatTime(time, SystemClock.elapsedRealtime());
}
/** @hide Just for debugging; not internationalized. */
public static String formatTime(long time, long referenceTime) {
long diff = time - referenceTime;
if (diff > 0) {
return time + " (in " + diff + " ms)";
}
if (diff < 0) {
return time + " (" + -diff + " ms ago)";
}
return time + " (now)";
}
/**
* Convert a System.currentTimeMillis() value to a time of day value like
* that printed in logs. MM-DD HH:MM:SS.MMM
*
* @param millis since the epoch (1/1/1970)
* @return String representation of the time.
* @hide
*/
public static String logTimeOfDay(long millis) {
Calendar c = Calendar.getInstance();
if (millis >= 0) {
c.setTimeInMillis(millis);
return String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c);
} else {
return Long.toString(millis);
}
}
/** {@hide} */
public static String formatForLogging(long millis) {
if (millis <= 0) {
return "unknown";
} else {
return sLoggingFormat.format(new Date(millis));
}
}
/**
* Dump a currentTimeMillis style timestamp for dumpsys.
*
* @hide
*/
public static void dumpTime(PrintWriter pw, long time) {
pw.print(sDumpDateFormat.format(new Date(time)));
}
/**
* This method is used to find if a clock time is inclusively between two other clock times
* @param reference The time of the day we want check if it is between start and end
* @param start The start time reference
* @param end The end time
* @return true if the reference time is between the two clock times, and false otherwise.
*/
public static boolean isTimeBetween(@NonNull LocalTime reference,
@NonNull LocalTime start,
@NonNull LocalTime end) {
// ////////E----+-----S////////
if ((reference.isBefore(start) && reference.isAfter(end)
// -----+----S//////////E------
|| (reference.isBefore(end) && reference.isBefore(start) && start.isBefore(end))
// ---------S//////////E---+---
|| (reference.isAfter(end) && reference.isAfter(start)) && start.isBefore(end))) {
return false;
} else {
return true;
}
}
/**
* Dump a currentTimeMillis style timestamp for dumpsys, with the delta time from now.
*
* @hide
*/
public static void dumpTimeWithDelta(PrintWriter pw, long time, long now) {
pw.print(sDumpDateFormat.format(new Date(time)));
if (time == now) {
pw.print(" (now)");
} else {
pw.print(" (");
TimeUtils.formatDuration(time, now, pw);
pw.print(")");
}
}}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 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;
/**
* Supplier for custom trace messages.
*
* @hide
*/
public interface TraceNameSupplier {
/**
* Gets the name used for trace messages.
*/
@NonNull String getTraceName();
}