Compare commits

...

409 Commits

Author SHA1 Message Date
achmad 06e93fd7bd chore: add Docker build config, scripts, and private registry setup
CI build / Validate Gradle Wrapper (push) Successful in 2m30s
CI build / jlink (linux-x64, ubuntu-latest) (push) Failing after 1m12s
CI build / Build Jar (push) Failing after 2m18s
CI build / jlink (macOS-arm64, macos-15) (push) Has been cancelled
CI build / jlink (macOS-x64, macos-15-intel) (push) Has been cancelled
CI build / jlink (windows-x64, windows-latest) (push) Has been cancelled
CI build / Make linux-assets release (push) Has been cancelled
CI build / Make appimage release (push) Has been cancelled
CI build / Make debian-all release (push) Has been cancelled
CI build / Make linux-x64 release (push) Has been cancelled
CI build / Make macOS-arm64 release (push) Has been cancelled
CI build / Make macOS-x64 release (push) Has been cancelled
CI build / Make windows-x64 release (push) Has been cancelled
CI build / release (push) Has been cancelled
2026-05-10 11:45:24 +07:00
achmad 02cac5df10 Merge branch 'master' of https://github.com/Suwayomi/Suwayomi-Server 2026-05-10 02:42:37 +07:00
renovate[bot] 6ee3348f50 Update javalin to v7 (major) (#1920)
* Update javalin to v7

* Update Javalin usage to v7 and Jackson 3

* Import fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2026-05-09 14:52:00 -04:00
achmad 8780597091 feat: add /image endpoint for chapter pages with Content-Length header
New GET endpoint at /api/v1/manga/{mangaId}/chapter/{chapterIndex}/page/{index}/image
that buffers the image into a byte array and sets Content-Length, enabling
progress tracking in Tachiyomi-based Android extensions.
2026-05-10 01:29:05 +07:00
renovate[bot] c98899d501 Update plugin shadowjar to v8.3.10 (#2007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 12:01:08 -04:00
renovate[bot] 7654653a25 Update plugin buildconfig to v6.0.9 (#2006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 12:00:44 -04:00
renovate[bot] 0c1a0ef408 Update Gradle to v9.5.0 (#2004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 12:00:30 -04:00
renovate[bot] e7f2192579 Update plugin ktlint to v14.2.0 (#2005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:59:40 -04:00
renovate[bot] 5c3b1e0b07 Update moko to v0.26.4 (#2003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:58:02 -04:00
renovate[bot] 6ad59f2e2b Update dex2jar to v2.4.36 (#2001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:51:32 -04:00
renovate[bot] 03dd778fac Update jte to v3.2.4 (#2002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:50:46 -04:00
renovate[bot] 6b833a38d1 Update kotlinx-coroutines monorepo to v1.11.0 (#1995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:47:26 -04:00
renovate[bot] a83885353c Update dependency com.typesafe:config to v1.4.8 (#1922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:46:20 -04:00
renovate[bot] 10d7c7c06d Update polyglot to v25 (#1651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:45:53 -04:00
Shozikan 3b575271cb Chore: Updated Moku's Description & Quick Spelling Fix (#1999)
Corrected spelling of 'abandoned' and clarified Moku's server management capabilities.
2026-05-09 11:44:29 -04:00
renovate[bot] 5ad92413f3 Update GitHub Artifact Actions (#1986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:44:11 -04:00
renovate[bot] 7fbdb39319 Update dependency io.github.oshai:kotlin-logging-jvm to v8.0.02 (#1983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:44:02 -04:00
renovate[bot] 92abdf7fb7 Update dependency com.auth0:java-jwt to v4.5.2 (#1980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:43:47 -04:00
renovate[bot] ea0c666cfe Update dependency com.android.tools.build:apksig to v9.2.1 (#1984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:43:36 -04:00
renovate[bot] b5e395a039 Update dependency com.ibm.icu:icu4j to v78.3 (#1985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:43:25 -04:00
renovate[bot] e530072a07 Update softprops/action-gh-release action to v3 (#1987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:43:14 -04:00
renovate[bot] f85cbe1ca5 Update dependency org.jsoup:jsoup to v1.22.2 (#1988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:43:04 -04:00
renovate[bot] 5f9126eb2f Update dependency org.postgresql:postgresql to v42.7.11 (#1989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:42:53 -04:00
renovate[bot] d77c57ede0 Update dependency androidx.annotation:annotation to v1.10.0 (#1990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:42:43 -04:00
renovate[bot] 02f9a0d1d7 Update dependency androidx.annotation:annotation to v1.10.0 (#1990)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:42:36 -04:00
renovate[bot] 53192f56ca Update serialization to v1.11.0 (#1996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:42:23 -04:00
renovate[bot] 01d89cbb48 Update dependency org.bouncycastle:bcprov-jdk18on to v1.84 (#1994)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:42:04 -04:00
renovate[bot] be55cb974b Update dependency io.insert-koin:koin-core to v4.2.1 (#1993)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:41:49 -04:00
renovate[bot] 34c394ed19 Update dependency com.squareup.okio:okio to v3.17.0 (#1992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:41:39 -04:00
renovate[bot] 1433a21abd Update graphqlkotlin to v8.9.0 (#1981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 11:41:33 -04:00
renovate[bot] ec28794655 Update kotlin to v2.3.21 (#1991)
* Update kotlin to v2.3.21

* Context Parameters

* Use new format

* Lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2026-05-09 11:41:27 -04:00
Syer10 72122b7cbf [skip ci] Update Changelog 2026-05-08 17:32:44 -04:00
renovate[bot] 392a7990d2 Update dependency com.github.junrar:junrar to v7.5.10 (#1926)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 17:30:48 -04:00
schroda 0bdcf8b4ba Handle serving non-default webui with "bundled" channel (#1924)
Channel "bundled" only works with the default webui.
So force change the flavor and log a warning for information
2026-05-08 17:30:42 -04:00
renovate[bot] 8295440bfd Update twelvemonkeys to v3.13.1 (#1919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 17:30:36 -04:00
Syer10 e52aa6daf4 [skip ci] Fix WebUI changelog link 2026-05-08 16:52:34 -04:00
Syer10 ef067ef5b9 Release v2.2.2100 2026-05-08 16:48:27 -04:00
Constantin Piber 76686db6a1 [#1974] Uninstall extension completely on install failure (#1975)
* [#1974] Uninstall extension completely on install failure

* Add changelog entry

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2026-05-05 10:04:50 -04:00
lamaxama 6bc5046773 Fix java.lang.VerifyError (#1972)
* Fix EditText.java

* Update CHANGELOG.md
2026-05-05 10:04:13 -04:00
Constantin Piber 1a5cfd8f58 Implement setPixel & setPixels (#1971)
* Implement `setPixel` & `setPixels`

Closes #1970

* Update changelog
2026-05-05 10:03:56 -04:00
schroda bf76962d23 [skip ci] Add github pr template (#1976) 2026-05-03 17:15:04 -04:00
schroda a8acca6a38 [skip ci] Update client section in readme (#1977)
Makes it so that the client section is less likely to get outdated and therefore requires less maintenance

Provides only information about how the clients can be run.
The client repo itself is responsible for providing any other information.

Remove clients that have not had any commits in years
2026-05-03 17:14:43 -04:00
schroda 7891c627c1 [skip ci] Update Preview Changelog (#1969) 2026-05-03 13:47:57 -04:00
schroda ee55145e45 [skip ci] Align changelog with webUI repo changelog (#1968) 2026-05-02 18:16:31 -04:00
schroda 5cda584568 Inject html base tag directly (#1967)
Using a script to inject the base tag is unnecessarily complex as well as it is introducing an issue where the initial requests will potentially fail, due to the base tag not being injected yet.

See https://github.com/Suwayomi/Suwayomi-WebUI/issues/1096, same issue applies when a subpath is set up which can't be fixed on the client side
2026-05-02 17:21:23 -04:00
AwkwardPeak7 031890deb6 extract apk icon (#1966) 2026-05-02 17:21:13 -04:00
ItsGlassPlus1 0f149c9b33 Add JXL container format support (#1951) 2026-05-02 17:21:01 -04:00
manti 41f22df16f Singleton Protobuf (#1961) 2026-05-02 17:20:52 -04:00
Shozikan a11e5e623d [skip ci] Chore: Added Moku to README & Quick Grammar/Formatting Fixes (#1935)
* Chore: Added Moku to README & Quick Grammar/Formatting Fixes

* Chore: Updated README with Moku Desc Changes
2026-03-31 16:43:51 -04:00
renovate[bot] 489ffa1679 Update dependency io.github.oshai:kotlin-logging-jvm to v8 (#1913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 20:09:45 -05:00
David Brochero f977d181a8 fix: default body to empty string if not present for FlareSolverr POST requests (#1915)
* fix: convert `RequestBody` to `FormBody` in FlareSolverr `POST` requests

* linting

* ref: don't convert json to form

* remove unused import
2026-02-20 20:09:34 -05:00
David Brochero 2249d237dd fix: support for POST requests on CloudflareInterceptor (#1909)
* fix: support for POST requests

Works with Flaresolverr. Required for Kagane.

Byparr is not a drop-in replacement, it just ignores the `cmd`  and interprets everything as a GET request.

* Use encodeToString instead

* linting

* Use FormBody for encoding

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

* Add missing imports

* linting, again

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2026-02-18 18:51:07 -05:00
renovate[bot] c52457c80e Update dependency com.auth0:java-jwt to v4.5.1 (#1910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:50:22 -05:00
renovate[bot] 3904cbf789 Update plugin download to v5.7.0 (#1908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:50:10 -05:00
renovate[bot] 759ae9fca0 Update moko to v0.26.0 (#1907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:57 -05:00
renovate[bot] 06954591c7 Update dependency org.postgresql:postgresql to v42.7.10 (#1904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:47 -05:00
renovate[bot] bbdae74567 Update dependency net.lingala.zip4j:zip4j to v2.11.6 (#1902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:27 -05:00
renovate[bot] 154e54d833 Update dependency ch.qos.logback:logback-classic to v1.5.32 (#1901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:21 -05:00
Weblate (bot) f18e0f4a62 Translations update from Hosted Weblate (#1845)
* Weblate translations

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: Roland Vezsenyi <miscogd5yf2paqvxvc@farvoid.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: 圭紫 <kaceykoo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

* Deleted translation using Weblate (Hungarian)

---------

Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: Roland Vezsenyi <miscogd5yf2paqvxvc@farvoid.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: 圭紫 <kaceykoo@gmail.com>
2026-02-14 11:38:22 -05:00
Constantin Piber 2b19bc850d Introduce Rect.set (#1900)
* Introduce `Rect.set`

As used by Young Jump+

Also fixes the `Rect(Rect)` constructor to use the correct values

* Missing line
2026-02-14 11:36:28 -05:00
renovate[bot] a0fb30a3ad Update jte to v3.2.3 (#1862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:36:16 -05:00
renovate[bot] e5387ff5f7 Update kotlin to v2.3.10 (#1896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:52 -05:00
renovate[bot] 123d8a2637 Update serialization to v1.10.0 (#1866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:38 -05:00
renovate[bot] 6c72659bd8 Update dependency com.android.tools.build:apksig to v9 (#1859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:30 -05:00
Mitchell Syer 44d89506d4 [skip ci] Fix Wiki PR Check (#1897) 2026-02-08 15:23:40 -05:00
Constantin Piber d8a5cdfb78 catch_abort: Remove java interop & catch SIGILL (#1891)
* catch_abort: Remove java interop

It won't work anyway since we're exiting the thread immediately

* catch_abort: Also catch SIGILL
2026-02-08 15:05:06 -05:00
schroda 5b5e2b26f9 Fix stale manga data in library update subscription (#1889)
Regression 439e0c8284
2026-02-08 15:04:59 -05:00
Constantin Piber 9fa51c8a1d fix: Match parameter nullability of WebView.java in KcefWebViewProvider (#1887) 2026-02-08 15:04:41 -05:00
renovate[bot] 7633d2156a Update dependency ch.qos.logback:logback-classic to v1.5.28 (#1884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 15:04:34 -05:00
renovate[bot] 400e059455 Update Gradle to v9.3.1 (#1883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 15:04:28 -05:00
schroda f3d6bb4f22 Add mutations to update multiple metas (#1874) 2026-02-08 15:04:19 -05:00
renovate[bot] c50b5e7448 Update dependency io.mockk:mockk to v1.14.9 (#1878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:48:56 -05:00
renovate[bot] 5b75b361f6 Update dependency ch.qos.logback:logback-classic to v1.5.26 (#1875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:48:45 -05:00
Mitchell Syer 146290283f ChildFirstClassLoader (#1873)
* ChildFirstClassLoader

* Lint

* ChildFirstURLClassLoader

* Fix reference
2026-01-29 18:48:33 -05:00
schroda 35bcbc69a2 Fix missing PostgreSQL schema migration (#1882)
I mistakenly thought that the suwayomi schema was always being used and therefore did not think a migration for the fix b4595b70d6 was necessary.
However, this was only the case in case the username was set to suwayomi. Otherwise, the public schema was being used.
Thus, we need to migrate the data from the public schema to the suwayomi schema in these cases.
2026-01-29 18:48:24 -05:00
schroda a58dcc6f19 Prevent subpath injection with disabled setting (#1869) 2026-01-23 11:30:29 -05:00
schroda b4595b70d6 Fix/using postgresql with hikaricp (#1867)
* Set default schema for postgresql db

The schema was only set once during startup. This is, however, only set for the current connection. So when using hikaricp, depending on which connection was used, the schema might have been set, or it might not have been set.

fixes #1670

* Revert "Fix database connection and errors (#1681)"

This reverts commit 2e0f72f182.

Not necessary anymore as the issue that this change intended to fix is now fixed with 091206800025ed9370d611e7ca3430ab409a0cb2
2026-01-22 20:27:22 -05:00
renovate[bot] 347b6faa82 Update dependency ch.qos.logback:logback-classic to v1.5.25 (#1864)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 20:27:04 -05:00
renovate[bot] 162a6b70af Update dependency org.postgresql:postgresql to v42.7.9 (#1860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 20:26:54 -05:00
renovate[bot] 3b0a05126b Update Gradle to v9.3.0 (#1861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 20:26:34 -05:00
Constantin Piber 02da884f17 fix: Do not return inputStream from conversion & handle conversion error (#1854)
* fix: Do not return `inputStream` from conversion

The returned value must be owned, since the caller closes the input
stream on success

* fix: Assume a conversion error consumes the input stream

e.g. converting an ARGB png to jpeg will throw "bogus colorspace", but
only after the inputstream is consumed. so in case of an exception, we
have to assume that the stream is broken and re-open the page from cache
2026-01-11 16:48:16 -05:00
Constantin Piber c4d8bba5ca Bitmap: Allow pixel-based access (#1855) 2026-01-11 16:48:07 -05:00
Constantin Piber b979db9acb BitmapFactory: Support basic options (#1853)
* BitmapFactory: Support basic options

* Bitmap: Support querying image type

* Bitmap: Support all BufferedImage image types

Required to be able to construct a bitmap with exactly the same
parameters
2026-01-11 16:47:52 -05:00
renovate[bot] b35c120bd1 Update dependency com.ibm.icu:icu4j to v78.2 (#1850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 16:47:40 -05:00
renovate[bot] 9e438566e4 Update dependency ch.qos.logback:logback-classic to v1.5.24 (#1848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-11 16:47:32 -05:00
Constantin Piber 3c518707eb Validate that all options are documented & Document missing settings (#1847)
* gha: Validate that all options are documented

* Document missing settings
2026-01-11 16:47:26 -05:00
Constantin Piber f8bee14808 Minor updates to Docs (#1846)
* Remove link to reference config

We don't include that any more, since it's now generated at build time

* Update outdated note regarding backup in Database section

Since #1682, the caution block is not true anymore
2026-01-11 16:47:15 -05:00
Constantin Piber 9f60bb8f3e Fix Kitsu to use library_id properly (#1843)
Same as mihonapp/mihon#2609
2026-01-03 12:54:49 -05:00
Mitchell Syer 7eb752654b Shikimori tracker (#1839)
* Shikimori tracker

* Add authUrl and callback

* Add OAuth id and secret
2026-01-03 12:54:30 -05:00
renovate[bot] a9d27acce3 Update dex2jar to v2.4.34 (#1833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:54:15 -05:00
renovate[bot] 3234c41333 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.14 (#1841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:54:04 -05:00
Weblate (bot) 339b28e7fb Translations update from Hosted Weblate (#1844)
* Weblate translations

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

* Deleted translation using Weblate (Persian)

---------

Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
2026-01-03 12:53:53 -05:00
schroda c624c6d860 Fix updating webui version update timestamp on manual update trigger (#1837) 2026-01-03 12:53:26 -05:00
Weblate (bot) 0cac39c19f Translations update from Hosted Weblate (#1836)
* Weblate translations

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

* Deleted translation using Weblate (Persian)

---------

Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
2026-01-03 12:53:14 -05:00
renovate[bot] 37bd5c32ab Update dependency org.jsoup:jsoup to v1.22.1 (#1840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:51:37 -05:00
renovate[bot] fc02f96b18 Update twelvemonkeys to v3.13.0 (#1832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:51:27 -05:00
renovate[bot] 27a83c4915 Update plugin buildconfig to v6.0.7 (#1830)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:51:21 -05:00
renovate[bot] 68006c4e68 Update dependency ch.qos.logback:logback-classic to v1.5.23 (#1828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 12:51:15 -05:00
Fred Silberberg b97a808e7b Implement android.util.LruCache (#1821)
* Add LruCache source from AOSP.

* Minimal changes for LruCache to compile
2025-12-17 15:28:10 -05:00
renovate[bot] 484f75374d Update kotlin monorepo to v2.3.0 (#1823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:27:58 -05:00
renovate[bot] f45f6faf96 Update dependency org.ow2.asm:asm to v9.9.1 (#1820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:27:51 -05:00
renovate[bot] 57fef90bc3 Update dependency com.android.tools.build:apksig to v8.13.2 (#1814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:27:38 -05:00
renovate[bot] e51ccdf2f0 Update dependency ch.qos.logback:logback-classic to v1.5.22 (#1815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:27:32 -05:00
renovate[bot] f39cf0f0f5 Update GitHub Artifact Actions (#1816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 15:27:24 -05:00
Kolby Moroz Liebl 817589f710 [skip ci] Bump deprecated publish workflow MacOS Version (#1811) 2025-12-08 21:33:32 -05:00
renovate[bot] b6e79532a9 Update dependency org.bouncycastle:bcprov-jdk18on to v1.83 (#1809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:42:35 -05:00
renovate[bot] 61ca48ccdd Update dependency io.mockk:mockk to v1.14.7 (#1808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:39:05 -05:00
schroda 3b36ec550d Remove koreader sync server address from settings (#1807)
Should have been done with 4dbd9d70d2
2025-12-06 12:38:45 -05:00
schroda 39cae6cc2d Fix server settings backup creation (#1806)
The "download conversions headers" caused a SerializationException.

kotlinx.serialization.SerializationException: 'null' is not supported as the value of collection types in ProtoBuf
2025-12-06 12:38:34 -05:00
schroda 4478042f40 Fix updating server settings (#1805)
The server setting updater passed an already converted value to the setting validator, which then tried to convert the value again, which caused an error

Regression aa8d27f679
2025-12-06 12:38:23 -05:00
robo a1ee1458e3 pass args in suwayomi-launcher scripts (#1801)
* pass args in suwayomi-launcher.sh

* pass args in Suwayomi Launcher.bat

* pass args in Suwayomi Launcher.command

i have no idea if this is correct
2025-12-06 12:38:12 -05:00
renovate[bot] 78b50b0881 Update plugin buildconfig to v6 (#1799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:38:02 -05:00
renovate[bot] 2af88056f2 Update actions/checkout action to v6 (#1798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:37:52 -05:00
renovate[bot] b20215731f Update moko to v0.25.2 (#1797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:37:41 -05:00
renovate[bot] a76959c295 Update Gradle to v9.2.1 (#1796)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 12:37:29 -05:00
renovate[bot] 83c94c044d Update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0 (#1786)
* Update dependency com.pinterest.ktlint:ktlint-cli to v1.8.0

* Format

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-12-06 12:37:19 -05:00
renovate[bot] 949829ae6b Update dex2jar to v2.4.33 (#1795)
* Update dex2jar to v2.4.33

* Update ASM

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-12-06 12:37:06 -05:00
schroda 9d7f54be82 Feature/improve server config non privacy safe setting handling (#1794)
* Move the "group" arg at the second position after "protoNumber"

To make it consistent for all settings

* Improve server config non privacy safe setting handling

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-11-25 21:58:00 -05:00
schroda aa8d27f679 Fix/backup restore with invalid settings (#1793)
* Fix typo in setting validation error message

* Convert value to internal type before validating it

* Only update setting in case value is valid

* Ignore settings validation errors on backup restore

* Remove potential not privacy safe value from logs
2025-11-25 21:56:57 -05:00
schroda b58a716daa Provide webui update timestamp in about webui gql query (#1789)
Makes it possible for the client to determine if a forced page refresh is required to ensure up-to-date webui files are being used
2025-11-25 21:56:39 -05:00
renovate[bot] ce9d080469 Update okhttp monorepo to v5.3.2 (#1790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 21:56:14 -05:00
renovate[bot] 34d0ce69fa Update dependency com.squareup.okio:okio to v3.16.4 (#1787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 21:55:56 -05:00
renovate[bot] b8842054a5 Update plugin ktlint to v14 (#1782)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 21:55:41 -05:00
renovate[bot] 966b39e039 Update dependency com.android.tools.build:apksig to v8.13.1 (#1781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 21:55:10 -05:00
renovate[bot] 2fcbe44953 Update dependency ch.qos.logback:logback-classic to v1.5.21 (#1780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 21:54:56 -05:00
Weblate (bot) f2795c972a Weblate translations (#1779)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Potorochin Max <ornaras.us@gmail.com>
2025-11-25 21:54:47 -05:00
Md Sadman Chowdhury 1a67d4db11 [skip ci] Fix broken link in README.md (#1788)
The "here" url to the sync section was broken. Fixed it.
2025-11-15 15:28:51 -05:00
Mitchell Syer 6c4c2a175b Allow using legacy database connections (#1785)
* Allow using legacy db connections

* Lint

* Description and docs
2025-11-12 16:24:41 -05:00
FadedSociety 0a7e6cce87 Remote Image Processing (#1684)
* Update ServerConfig.kt

* Update ConversionUtil.kt

* Update Page.kt

* Update ServerConfig.kt

fixed deletions caused by ide

* Update ServerConfig.kt

* Update ServerConfig.kt

* Cleanup

* Post-processing terminology

* More comments

* Lint

* Add known image mimes

* Fix weird mime set/get

* Implement different downloadConversions and serveConversions

* Lint

* Improve Post-Processing massivly

* Fix thumbnail build

* Use Array for headers

* Actually fix headers

* Actually fix headers 2

* Manually parse DownloadConversion

* Cleanup parse

* Fix write

* Update TypeName

* Optimize imports

* Remove header type

* Fix build

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-11-12 16:23:34 -05:00
Mitchell Syer 3e47859d88 Fix jdk.random error (#1775) 2025-11-08 19:48:46 -05:00
schroda 8e405e3996 Fix/manga update of title and download folder (#1774)
* Use "move" instead of "renameTo"

* Add additional checks for download folder rename

* Log exception during manga download folder rename
2025-11-08 19:38:35 -05:00
schroda 3b21442e25 Feature/web interface manager cache requests (#1773)
* Cache webui server mapping file

* Cache webui preview version
2025-11-08 19:38:25 -05:00
schroda dd895157c6 Throttle WebInterfaceManager requests retries (#1772) 2025-11-08 19:38:16 -05:00
schroda 436daeb87c Overwrite existing served webui files on copy (#1771)
There is a possibility that the serve folder was only partially deleted. This then could cause "FileAlreadyExistsException" when copying the webui files to the serve folder.
2025-11-08 19:38:08 -05:00
Mitchell Syer 00f5652db9 Use JDK 25 to build and include in releases (#1770)
* Use JDK 25 to build and include in releases

* Set Jvm Targets in BuildSrc

* Put JvmTarget in libs.versions.toml

* Maybe this will work for Zulu

* Use LTS java version
2025-11-08 19:37:59 -05:00
Weblate (bot) c452a3548f Translations update from Hosted Weblate (#1765)
* Weblate translations

Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Potorochin Max <ornaras.us@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/it/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

* Add new languages

---------

Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
Co-authored-by: Potorochin Max <ornaras.us@gmail.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-11-08 19:37:49 -05:00
renovate[bot] 1dc1932a84 Update Gradle to v9 (#1558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-08 18:30:57 -05:00
renovate[bot] 3941986ab2 Update plugin buildconfig to v5.7.1 (#1761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 15:26:31 -05:00
Mitchell Syer 0f95e19c76 Fix websocket token (#1766) 2025-11-06 15:26:04 -05:00
Mitchell Syer bb09c558b6 Fix KoReader Sync User Deprecation (#1760) 2025-11-01 17:59:29 -04:00
Syer10 ed7e9f4a2e Trigger Build 2025-11-01 16:10:45 -04:00
schroda 6b0fddd421 Prevent running webui auto update in parallel to the setup (#1759) 2025-11-01 14:31:13 -04:00
schroda 4dbd9d70d2 Fix/remove koreader-sync credentials from server config (#1758)
* Remove koreader-sync credentials from config

These are supposed to be set via the login/logout mutations and are not meant to be set manually by the user. Thus, they are not really settings and do not belong to the config

* Reduce log levels of KoreaderSyncService
2025-11-01 14:31:07 -04:00
schroda 53c4659044 Optionally allow basic auth fallback for chapter page endpoint (#1757)
Requesting a chapter page while having a non basic auth mode (e.g. ui login) enabled caused the basic auth prompt.
However, this is only supposed to be happen for opds
2025-11-01 14:31:01 -04:00
schroda fb41ad38f6 Ensure webui static resources folder exists before serving it (#1756)
When the webUI got opened before the setup was completed, the missing folder caused an exception and broke the javalin server.
In that case, even after the webui setup completed, the server just returned the index.html for every path
2025-11-01 14:30:54 -04:00
renovate[bot] ebcb9a9562 Update okhttp monorepo to v5.3.0 (#1755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 14:30:38 -04:00
renovate[bot] 678a12490b Update dependency com.ibm.icu:icu4j to v78 (#1754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 14:30:32 -04:00
Weblate (bot) 4978461ac7 Weblate translations (#1753)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/it/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
2025-11-01 14:30:14 -04:00
Constantin Piber d63965cc02 Include jogamp native files in all bundles (#1752) 2025-11-01 14:29:54 -04:00
Constantin Piber 2a27491f9f [#1749] Only consider path in SIMPLE_LOGIN redirect (#1751)
* [#1749] Only consider path in SIMPLE_LOGIN redirect

We don't really care about the origin, since that is implicit in
`Location` headers if not given, which sidesteps the HTTP/HTTPS issue

* Refuse non-relative redirects
2025-11-01 14:29:28 -04:00
renovate[bot] 748f0494b4 Update xmlserialization to v0.91.3 (#1748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-01 14:29:10 -04:00
Mitchell Syer c9575449da Allow basic auth fallback for OPDS (#1745) 2025-10-26 12:38:02 -04:00
Constantin Piber 3e7c5e2948 Revert "Support sending floats" and convert DoubleFilter to FloatFilter (#1747)
* Revert "[#1739] Support sending floats (#1740)"

This reverts commit c1f2aae90d.

Closes #1746

* Use `DoubleFilter` for GQL interface, convert to `FloatFilter`

Closes #1739 (again)
2025-10-26 12:37:50 -04:00
Constantin Piber 21e64eb54a Introduce a MainCoroutineDispatcher (#1744)
* Introduce a `MainCoroutineDispatcher`

This is almost entirely based on
https://github.com/Kotlin/kotlinx.coroutines/blob/8c27d51025d56a7b8de8eec2fb234b0094ce84f1/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt,
which is the implementation of `kotlinx-coroutines-android`

* Lint

* Move dispatcher to AndroidCompat
2025-10-25 14:31:59 -04:00
renovate[bot] c4b2f8582e Update GitHub Artifact Actions (#1741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 18:38:59 -04:00
Constantin Piber c1f2aae90d [#1739] Support sending floats (#1740)
* [#1739] Support sending floats

Required for `FloatFilter`, which needs to be float due to DB layer

* Simplify `parseValueImpl`, use correct coercion hints
2025-10-24 18:38:41 -04:00
Constantin Piber 9d09a1fe5d [#1596] Dispose KCEF on shutdown (#1738)
* [#1596] Dispose KCEF on shutdown

* Use blocking variant
2025-10-24 18:38:27 -04:00
Constantin Piber 7d006a19c3 android.util.Log: Pass log level to actual logger (#1737)
Instead of just printing it and dumping everything at INFO, call the
underlying logger with the correct level
2025-10-24 18:38:19 -04:00
Constantin Piber d23d10601e Add --change-stack-guard-on-fork=disable to fix stack smashing (#1736)
* Add --change-stack-guard-on-fork=disable to fix stack smashing

See chromiumembedded/cef#3912

* Add PR comments
2025-10-24 18:38:04 -04:00
Constantin Piber b4db9ebdb0 Settings generator: Validate that protoNumber is unique (#1735)
This makes specifying an already-used number a compile time error
2025-10-24 18:37:58 -04:00
renovate[bot] d8050dc483 Update kotlin monorepo to v2.2.21 (#1733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 18:37:52 -04:00
Zeedif 4011a4c3ee feat(koreader): Set public server as default for KOReader Sync (#1732) 2025-10-24 18:37:22 -04:00
Weblate (bot) c35dd1a2c7 Weblate translations (#1728)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/it/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Gino Cicatiello <ginocic@gmail.com>
Co-authored-by: Kamil Czesny <maczete2@gmail.com>
2025-10-24 18:37:15 -04:00
Constantin Piber bc6e28cabe OPDS: Offer CBZ in older mimetype (#1731)
* OPDS: Offer CBZ in older mimetype

* OPDS: Include length when offering CBZ download

* Disable compression on CBZ endpoint

Zipping a zip

* CBZ download match content type of OPDS

* Move compression disable

* Introduce setting for configuring CBZ mimetype

* Document new option

[no-ci]

* Update server/src/main/kotlin/suwayomi/tachidesk/opds/impl/OpdsEntryBuilder.kt

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

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-10-24 18:36:59 -04:00
Constantin Piber 68492bf591 Actually set Track ID when updating track (#1726) 2025-10-20 13:56:08 -04:00
renovate[bot] 29e7bab4e4 Update dependency ch.qos.logback:logback-classic to v1.5.20 (#1727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 13:55:58 -04:00
Constantin Piber 5be4d2a104 Minor bug fixes for Webview, Permission request support (#1723)
* fix: Match URLs with trailing /

* Handle permission requests and attempt to enable Widevine

* Tie CEF loglevel to server debug logs

* Lint

* Add missing file

Forgot to add in previous commits

* Provide WebResourceResponse

* Fix NullException if headers are not set

* fix: Don't allow interception for initial page load

fixes #1713

* Lint
2025-10-17 12:07:18 -04:00
Mitchell Syer 0585000cf3 Fix header/cookie based websocket auth (#1722)
* Fix header/cookie based websocket auth

* Lint
2025-10-17 12:07:02 -04:00
renovate[bot] 045e4d23fb Update dependency com.github.junrar:junrar to v7.5.7 (#1721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-17 12:06:46 -04:00
schroda 7b5d96189e Feature/automatic backup flags (#1702)
* Add backup flags to auto backups

* Mark ServerConfig properties as deprecated

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-10-14 19:40:46 -04:00
renovate[bot] 5b46359057 Update okhttp monorepo to v5.2.1 (#1706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 19:40:04 -04:00
renovate[bot] 44de584be3 Update plugin buildconfig to v5.7.0 (#1707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 19:39:51 -04:00
renovate[bot] 52a9be3a7e Update dependency com.squareup.okio:okio to v3.16.2 (#1708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 19:39:11 -04:00
schroda 8d119fd710 Feature/cleanup backup logic (#1701)
* Extract global metadata backup logic into BackupGlobalMetaHandler

* Extract category backup logic into BackupCategoryHandler

* Extract source backup logic into BackupSourceHandler

* Extract manga backup logic into BackupMangaHandler
2025-10-14 19:37:29 -04:00
Weblate (bot) b802c6977a Weblate translations (#1705)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/fr/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: mrintrepide <quentin@intrepide.io>
2025-10-14 19:37:22 -04:00
robo 1af1562473 [skip ci] Update arch linux install in README.md (#1716) 2025-10-11 19:07:52 -04:00
schroda 0d79ac68f8 Feature/backup import add backup flags (#1697)
* Add backup flags to backup restore

* Cleanup default backup flags handling

* Optionally exclude manga from backup
2025-10-05 18:52:45 -04:00
Constantin Piber 3ce9f72e3f Allow symlinks in static files (#1699) 2025-10-04 15:12:59 -04:00
schroda 9437e4243a Use canonical path for static files directory (#1698)
On mac the temp system folder is a symlink which jetty does not allow by default due to security reasons.
This caused the webui files to not get served on mac.
2025-10-04 15:12:52 -04:00
Constantin Piber b92f9a2e4d [skip ci] Renovate json fix (#1696)
* Regex fix backslashes

* Add required `depNameTemplate`

It's not used, but renovate requires it
2025-10-03 10:57:20 -04:00
Constantin Piber 6181467abb Include jdk.accessibility in jlink setup (#1693)
Same as mguessan/davmail#417
2025-10-03 10:39:44 -04:00
renovate[bot] 5b55858184 Update moko to v0.25.1 (#1691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:39:29 -04:00
renovate[bot] 6dd789dedc Update gradle/actions action to v5 (#1690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:39:11 -04:00
renovate[bot] 8d3a144afd Update dependency io.mockk:mockk to v1.14.6 (#1689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:38:49 -04:00
renovate[bot] 5577dff087 Update dependency ch.qos.logback:logback-classic to v1.5.19 (#1688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-03 10:38:37 -04:00
schroda f4e32bac1a Clear queued state updates on immediate emission (#1685)
There was a possible race condition where immediate state updates got overwritten by previously queued ones.
For example, when the download was successful but the downloaded files are deemed invalid, a previously queued download progress state update might overwrite the emitted error state.
2025-10-03 10:38:25 -04:00
Constantin Piber 02aada7f08 [#1676] Reorder source pages (#1683)
Some sources don't properly index their pages in some situations, so
follow Mihon and reindex
2025-10-03 10:38:10 -04:00
Mitchell Syer fb05371ac2 Add way to exclude settings from backups (#1682)
* Add way to exclude settings from backups

* Exclude flaresolverrEnabled

* Exclude usernames/passwords

* Exclude writing deprecated settings to the backup

* Exclude AuthMode
2025-10-03 10:37:50 -04:00
Constantin Piber 9a9c0532de Move to Zulu JRE (#1675)
* Move `apt` commands behind `$CI`

Same as for the windows command, no need to rerun this every time

* Use fallback to avoid unbound variables

* Scripts: Switch to Zulu JRE

* workflows: Switch to zulu JDK

* fix: Launcher requires sun.awt.* exports

* Update renovate for Zulu
2025-10-03 10:37:30 -04:00
Mitchell Syer 2e0f72f182 Fix database connection and errors (#1681) 2025-09-29 11:24:52 -04:00
schroda f210bbc22a Setup webUI in background (#1679)
Stops blocking javalin startup in case of internet connection issues
2025-09-29 11:24:45 -04:00
Constantin Piber 08c4f8bcc2 Clarify UI_LOGIN availability and introduce bullet points (#1677) 2025-09-29 11:24:32 -04:00
schroda 9062252939 Fix/server config duplicated types (#1672)
* Move "serverConfig" to "server-config" module

* Remove duplicated types

Unintentionally introduced with 8ef2877040
2025-09-29 11:24:26 -04:00
schroda 5c79672d84 Use graphql directive for auth handling (#1671) 2025-09-29 11:24:19 -04:00
schroda 5e48b05270 [skip ci] Add database to bug issue template (#1680) 2025-09-28 18:24:29 -04:00
schroda 28ab0af6d4 Fix base path injection when serving from root (#1669)
Without having a subpath defined, the base path incorrectly ended with two "/" instead of one
2025-09-25 11:56:19 -04:00
schroda cdb98d2175 Feature/reduce logging (#1667)
* Reduce Hikari related logging

* Reduce WebView related logging
2025-09-24 18:01:23 -04:00
schroda d95f4fe1e1 Fix/webui subpath injection (#1666)
* Cleanup subpath handling

* Move webUI serve setup logic to WebInterfaceManager

* Fix webUI subpath injection

Dynamic subpath support on the client requires using relative paths for everything.
Without a <base> tag this only works when opening the client on the root path.
Any subpath will result in a blank page because the used url to request e.g., an asset will be invalid and cause an error (type mismatch, since the index.html will be returned for any unmatch route).
2025-09-24 18:01:13 -04:00
Mitchell Syer 6e2be271c3 Minor DB Fix (#1668)
* Minor DB fix

* Lint
2025-09-24 18:01:04 -04:00
Soner Köksal bfccbaf731 Optimize database performance with HikariCP and transaction batching (#1660)
* Optimize database performance with HikariCP and transaction batching

- Add HikariCP-7.0.2 connection pooling with Raspberry Pi optimized settings
- Consolidate database transactions in DirName, ChapterForDownload, and ChapterDownloadHelper
- Remove duplicate queries and unused methods from ChapterForDownload
- Batch database operations to reduce transaction overhead
- Add shared query functions to eliminate redundant database calls
- Configure memory settings for build optimization

Performance improvements:
- DirName functions: 99% faster (29s → 0.1s)
- ChapterDownloadHelper: 99.5% faster (54s → 0.3s)
- ChapterForDownload: 97% faster transaction operations
- Overall system: 75% faster execution time (242s → 60s)

* Fix review comments
2025-09-23 15:54:09 -04:00
Soner Köksal c7b4f226b3 Add server-side subpath support for WebUI (#1658)
* Add server-side subpath support for WebUI

- Add webUISubpath configuration setting with regex validation
- Create temporary WebUI directory for subpath serving
- Inject subpath config into index.html for client detection
- Support all WebUI flavors (WEBUI, VUI, CUSTOM) with subpath
- Update browser opening to use subpath URLs

Related to Suwayomi/Suwayomi-WebUI#174

* Fix review points

* Fix code formatting issues

* Fix import issue
2025-09-23 15:53:53 -04:00
renovate[bot] d142d3a25a Update dependency org.postgresql:postgresql to v42.7.8 (#1657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 15:53:44 -04:00
renovate[bot] f8b00a4541 Update dependency org.bouncycastle:bcprov-jdk18on to v1.82 (#1652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 15:53:38 -04:00
Mitchell Syer 808e0ecae7 Make Sure Backup Create Flags are Exposed (#1650) 2025-09-15 16:45:14 -04:00
Constantin Piber a09cac1063 Update wiki for settings changes (#1649)
* Update auth settings

* Add some missing config values

* Introduce KOReader variables

* Introduce Database variables
2025-09-15 10:21:53 -04:00
renovate[bot] 513af98872 [skip ci] Update actions/checkout action to v5 (#1648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 09:41:18 -04:00
Mitchell Syer fafcaa222c Update Dex2Jar (#1644) 2025-09-15 09:25:20 -04:00
Mitchell Syer 1dd79a0b1e Add Wiki to main repo to allow pull requests for improvements (#1647) 2025-09-15 09:25:13 -04:00
schroda bbd7e30298 Fix/server startup config update failure handling (#1646)
* Catch config value migration exception

In case the value did not exist in the config a "ConfigException.Missing" exception was thrown which caused the whole migration to fail.

Fixes #1645

* Improve config migration logging

* Update user config file after config update

The user config file gets reset before the update.
This could cause the user settings to get lost on the next server start in case something went wrong during the update and the updated config never got saved to the actual file.
2025-09-14 10:32:23 -04:00
renovate[bot] 2818fbe575 Update dependency com.typesafe:config to v1.4.5 (#1642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 12:22:28 -04:00
renovate[bot] a11db6662d Update kotlin monorepo to v2.2.20 (#1640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 12:22:22 -04:00
schroda 904157a91a Streamline deprecated settings config value migration logic (#1633)
* Streamline deprecated settings config value migration logic

* Add "autoDownloadAheadLimit" config migration

For consistency

* Replace "exitCode" with "shutdownApp"

* Enhance shutdown logging to include exit reason
2025-09-13 12:22:09 -04:00
renovate[bot] 904d3980d6 Update plugin ktlint to v13.1.0 (#1609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 12:21:58 -04:00
renovate[bot] 89e2ba9f75 Update dependency com.pinterest.ktlint:ktlint-cli to v1.7.1 (#1520)
* Update dependency com.pinterest.ktlint:ktlint-cli to v1.7.1

* Lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-09-13 12:21:49 -04:00
Mitchell Syer 3f4dd2861e Fix pages after refactoring downloads (#1639) 2025-09-09 21:10:27 -04:00
Weblate (bot) c1a9b158e2 Weblate translations (#1638)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pt/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Thiago Alencar <thiago.alcr@gmail.com>
2025-09-09 18:15:08 -04:00
schroda 7db947eba2 Restore server settings first (#1637)
The imported server settings might cause the active database to change.
In case this happens, the backup was restored into the wrong database.
2025-09-09 18:14:47 -04:00
renovate[bot] 450861d47a Update plugin buildconfig to v5.6.8 (#1636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 18:14:36 -04:00
schroda 3df0106325 Fix/logging user sensitive config data in cleartext (#1634)
* Redact username and passwords from config log

* Redact empty username and password

* Make regex Username/Password case-insensitive in config redaction
2025-09-09 18:14:21 -04:00
schroda 2b767eb488 Fix/local manga thumbnails handling (#1630)
* Skip thumbnail download for local manga sources

Local manga sources do not require downloading thumbnails as they are stored locally.

* Always update local source manga info when browsing

When making changes to an in library local source manga, a refresh from the source was required to get the latest data.
From a user perspective, this is unexpected behavior that looks like a bug.

If, for example, the thumbnail file extension got changed, the file could not be found anymore and an error was shown in the client. To fix this, a manga refresh was required.
2025-09-09 18:14:01 -04:00
renovate[bot] f8c77b3673 Update dependency io.insert-koin:koin-core to v4.1.1 (#1629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 18:13:50 -04:00
Mitchell Syer aaaae4e719 Handle null keys better (#1628) 2025-09-09 18:13:40 -04:00
Mitchell Syer 679e2c0da9 Optimize Download Queue (#1627)
* Optimize download Queue

* Lint

* Fix name of DownloadStatus file

* Re-add synchronous status fetch
2025-09-09 18:13:31 -04:00
renovate[bot] 055c1c47f6 Update dependency com.android.tools.build:apksig to v8.13.0 (#1626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 18:13:18 -04:00
Zeedif 275727ed90 feat(kosync): add mutations for manual progress push and pull (#1625)
Exposes the existing push and pull functionality from the KoreaderSyncService via the GraphQL API.

This change introduces two new mutations:
- `pushKoSyncProgress`: Manually sends the current chapter's reading progress to the KOReader sync server.
- `pullKoSyncProgress`: Manually fetches and applies the latest reading progress from the KOReader sync server.

These mutations enable clients and WebUIs to implement manual sync triggers, providing users with more direct control over their reading progress synchronization, similar to the functionality offered by the official KOReader plugin and other clients like Readest.
2025-09-09 18:13:05 -04:00
Zeedif 257e1dd03d refactor(kosync): introduce differentiated sync strategies (#1624)
* refactor(kosync): introduce differentiated sync strategies

Replaces the single `koreaderSyncStrategy` setting with `koreaderSyncStrategyForward` and `koreaderSyncStrategyBackward`. This allows users to define distinct conflict resolution behaviors based on whether the remote progress is newer or older than the local progress.

The `KoreaderSyncStrategy` enum has been simplified to `KoreaderSyncConflictStrategy` with four clear options: `PROMPT`, `KEEP_LOCAL`, `KEEP_REMOTE`, and `DISABLED`. The ambiguous `SILENT` option is removed, as its behavior is now implicitly covered by selecting `KEEP_REMOTE` for forward syncs and `KEEP_LOCAL` for backward syncs.

The legacy `koreaderSyncStrategy` setting is now deprecated and is seamlessly migrated to the new dual-strategy system using `MigratedConfigValue`, ensuring backward compatibility for existing user configurations.

* fix(kosync): correct proto numbers and setting order for sync strategies

* fix(kosync): proto number 78 to 68

* fix(server): migrate KOReader sync strategy during settings cleanup

Add migration logic to convert the old `server.koreaderSyncStrategy` key
into the new `server.koreaderSyncStrategyForward` and
`server.koreaderSyncStrategyBackward` keys during server setup.
2025-09-09 18:12:53 -04:00
Syer10 5bf2a4aed4 Set BackupServerSettings to nullable 2025-09-02 16:34:48 -04:00
Mitchell Syer dc79b4c90a Support PostgreSQL Databases (#1617)
* Support PostgreSQL Databases

* Set the database Schema

* See if we can test postgres

* Another test

* Disable node container

* Update database when changed

* Simplify test workflow

* Only exit on failed migrations

* Run the first databaseUp sync

* Map the port

* Use absolute path for LD_PRELOAD

* Timeout after 1m

* Open the server in both database configurations

* Only exit on migration failed in ci

* Lint

* Use new ServerConfig configuration
2025-09-02 12:29:09 -04:00
renovate[bot] 4cdc7b348d Update dependency com.android.tools.build:apksig to v8.12.2 (#1620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 17:03:33 -04:00
Mitchell Syer ddedceeded Support null preference keys (#1623) 2025-09-01 17:03:21 -04:00
renovate[bot] 3179169913 Update dependency org.jsoup:jsoup to v1.21.2 (#1615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 17:03:10 -04:00
schroda 8ef2877040 Feature/streamline settings (#1614)
* Cleanup graphql setting mutation

* Validate values read from config

* Generate server-reference.conf files from ServerConfig

* Remove unnecessary enum value handling in config value update

Commit df0078b725 introduced the usage of config4k, which handles enums automatically. Thus, this handling is outdated and not needed anymore

* Generate gql SettingsType from ServerConfig

* Extract settings backup logic

* Generate settings backup files

* Move "group" arg to second position

To make it easier to detect and have it at the same position consistently for all settings.

* Remove setting generation from compilation

* Extract setting generation code into new module

* Extract pure setting generation code into new module

* Remove generated settings files from src tree

* Force each setting to set a default value
2025-09-01 17:02:58 -04:00
Weblate (bot) 11b2a6b616 Weblate translations (#1605)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/de/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/es/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/vi/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/zh_Hans/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Christian Heinrich <ch.heinrich16@gmail.com>
Co-authored-by: Constantin Piber <cp.piber@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Weblate <allendandstart@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>
2025-09-01 17:02:38 -04:00
Constantin Piber 04ad0033d7 GraphQL directly uses getUserFromToken and expects a valid user for (#1616)
other authentication methods, so re-introduce that check
2025-08-25 06:02:12 -04:00
Constantin Piber 46e2ef125a OPDS: Allow fallback to Basic Auth (#1613)
* Move API authorization to UserType

We already verify the JWT there, so do the same with cookies. This makes
the next steps easier

* OPDS: Allow basic auth as fallback

* Send 404 for any unmatched API request

Redirecting to the UI is weird and can cause problems with the
SIMPLE_LOGIN check (which ignores API requests)

* Webview: Present Login page in SIMPLE_LOGIN mode

For BASIC_AUTH, the dialog is always presented. With UI_LOGIN, we have a
custom login dialog.
Before, SIMPLE_LOGIN would just say "Unauthorized", as with all API
endpoints. With the last commits, SIMPLE_LOGIN is checked by the
endpoints, which Webview did not, so the page would load, but then the
Websocket would error out, despite showing the login dialog.

* Lint
2025-08-24 12:36:11 -04:00
schroda 9a33e3808a Feature/graphql settings add jwt settings (#1612)
* Add jwt settings to grapqhl SettingsType

* Sort proto BackupServerSettings by ProtNumber
2025-08-24 12:35:59 -04:00
Zeedif 8ae451ece5 refactor(opds): align feed generation with RFC5005 and OpenSearch specs (#1611)
* refactor(opds): align feed generation with RFC5005 and OpenSearch specs

This commit refactors the OPDS feed generation to strictly adhere to official specifications for search and pagination.

Previously, OpenSearch response elements (totalResults, itemsPerPage, startIndex) were incorrectly included in all acquisition feeds. According to the OPDS 1.2 and OpenSearch 1.1 specifications, these elements should only be present in feeds that are a direct response to a search query. This change restricts their inclusion to search result feeds only, ensuring spec compliance.

Additionally, pagination link relations were not fully implemented as per RFC 5005. This commit enhances all paginated feeds to include `first` and `last` links, in addition to the existing `prev` and `next` links. This provides a complete and standard-compliant navigation experience for OPDS clients.

- `FeedBuilderInternal` now accepts an `isSearchFeed` flag to conditionally add OpenSearch elements.
- All feed generation methods in `OpdsFeedBuilder` and `OpdsV1Controller` now correctly identify search contexts.
- RFC 5005 pagination links (`first`, `last`, `prev`, `next`) are now generated for all paginated feeds.
- Added necessary link relation constants to `OpdsConstants`.

* feat(opds): improve pagination navigation and code organization
2025-08-24 12:35:47 -04:00
Zeedif a5d64be197 fix(kosync): use correct partial hash for binary checksum (#1610)
When generating a hash on-the-fly for the binary checksum method, the code was performing a full MD5 hash of the entire file stream. This was incorrect as the KOReader binary method expects a specific partial hash (reading small chunks at different offsets).

This change ensures that when an in-memory CBZ is created, it is first written to a temporary file. Then, the correct `KoreaderHelper.hashContents()` function is used on that file to generate the partial hash, matching KOReader's logic. The temporary file is deleted immediately after.
2025-08-24 12:35:38 -04:00
Constantin Piber 4482b325d7 [#1575] Disable Alt translation (#1608) 2025-08-21 19:18:33 -04:00
Zeedif f46745d70c feat(kosync): Implement On-the-Fly Deterministic Hashing for KOReader Sync (#1606)
* fix(archive): unify CBZ generation to produce deterministic archives

Previously, CBZ files generated on-the-fly (`FolderProvider`) had a different hash than those created directly (`ArchiveProvider`), even with identical content. This inconsistency was caused by using two different ZIP libraries (`java.util.zip` vs. `org.apache.commons.compress`) and not normalizing file metadata.

This inconsistent hashing breaks binary-based synchronization with external services like KOReader Sync Server, as the same chapter could be identified as a different file on each generation.

This change ensures CBZ generation is fully deterministic by:

- Unifying both providers to use `org.apache.commons.compress`.
- Setting a fixed epoch timestamp (`time = 0L`) for all ZIP entries.
- Explicitly setting the compression method and level to `DEFLATED` with default compression.

This guarantees that a CBZ file for a given chapter will always have the same hash, regardless of how it's generated, resolving synchronization issues.

* feat(kosync): lazily generate and cache CBZ hashes for sync

Previously, KOReader progress sync in binary mode was limited to chapters explicitly downloaded as CBZ files. Chapters stored as folders lacked a hash, preventing them from being synced.

With the recent move to deterministic CBZ generation, it's now possible to create a consistent hash for any downloaded chapter on-the-fly.

This commit enhances the `getOrGenerateChapterHash` function to act as a central point for hash management. If a hash is requested for a downloaded chapter that doesn't have one cached in the database:

1.  It generates the CBZ archive in-memory from the downloaded folder or existing CBZ using `ChapterDownloadHelper.getAsArchiveStream()`.
2.  It calculates the deterministic hash of the generated archive content.
3.  It saves this hash to the `koreader_hash` column in the `Chapter` table for future use.

The cached hash is cleared when the chapter download is deleted, ensuring hashes are only tracked for available content.

This change transparently extends Koreader Sync compatibility to all downloaded chapters, regardless of their storage format, without requiring users to pre-convert their library to CBZ.

* fix: rename getAsArchiveStream to getArchiveStreamWithSize
2025-08-21 19:18:27 -04:00
renovate[bot] b213de19cc Update actions/setup-java action to v5 (#1607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 19:18:08 -04:00
renovate[bot] 75355f0784 Update dependency com.auth0:java-jwt to v4.5.0 (#1603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 19:18:00 -04:00
Constantin Piber 8547159eec Basic JWT implementation (#1524)
* Basic JWT implementation

* Move JWT to UI_LOGIN mode and bring back SIMPLE_LOGIN as before

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

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

* Refresh: Update only access token

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

* Implement JWT Audience

* Store JWT key

Generates the key on startup if not set

* Handle invalid Base64

* Make JWT expiry configurable

* Missing value parse

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

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

* Simplify Duration parsing

* JWT Protect Mutations

* JWT Protect Queries and Subscriptions

* JWT Protect v1 WebSockets

* WebSockets allow sending token via protocol header

* Also respect the `suwayomi-server-token` cookie

* JWT reduce default token expiry

* JWT Support cookie on WebSocket as well

* Lint

* Authenticate graphql subscription via connection_init payload

* WebView: Prefer explicit token over cookie

This hack was implemented because WebView sent `"null"` if no token was
supplied, just don't send a bad token, then we can do this properly

* WebView: Implement basic login dialog if no token supplied

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
2025-08-20 18:04:48 -04:00
Weblate (bot) d90bfb6e3e Weblate translations (#1599)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/es/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/vi/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>
2025-08-20 18:03:46 -04:00
Zeedif 82ad2fbe80 feat(opds): Enhance KOSync Conflict Handling and Reliability (#1602)
* feat(opds): Enhance KOSync conflict handling and reliability

This commit introduces several improvements to the KOSync integration within the OPDS feed, focusing on fixing bugs, improving network stability, and enhancing user feedback during synchronization conflicts.

- fix(sync): Corrects a KOSync JSON deserialization issue by mapping the `updated_at` field from the server response to the `timestamp` property in the client's data model. This resolves a critical bug where remote progress was being ignored.
- fix(sync): Adds a `Connection: close` header to all KOSync API requests. This prevents `unexpected end of stream` errors by ensuring a fresh connection is used, improving network reliability.
- feat(opds): Resolve sync conflicts by generating separate OPDS entries for local and remote progress. This aligns with the OPDS-PSE specification's implicit design of one stream link per entry. Instead of incorrectly adding multiple links to a single entry, the feed now presents two distinct, clearly labeled entries, allowing users to choose their desired reading position from compatible clients.
- chore(sync): Adds detailed debug logging for KOSync `GET` and `PUT` requests, including request URLs, sent data, and received responses. This improves traceability and makes debugging future issues significantly easier.

* change synced icon

* unnecessary comments removed
2025-08-20 18:03:33 -04:00
Zeedif a414860626 fix(api): optimize HEAD requests for chapter downloads (#1601)
Previously, handling a HEAD request on the chapter download endpoint was inefficient as it triggered the full CBZ file generation process in-memory just to retrieve metadata like Content-Length and Content-Disposition. This caused unnecessary latency especially for OPDS clients.

This commit introduces a separate, lightweight path for HEAD requests.

- A new `getCbzMetadataForDownload` method is added to `ChapterDownloadHelper` to calculate the filename and file size without generating an archive stream.
- The `ChaptersFilesProvider` interface is updated with a `getArchiveSize()` method, implemented by both `ArchiveProvider` and `FolderProvider`, to retrieve the total size.
- The `MangaController` now differentiates between GET and HEAD methods, invoking the appropriate helper to ensure HEAD requests are served instantly with only the required metadata.
2025-08-20 18:03:26 -04:00
renovate[bot] 392f66e938 Update plugin shadowjar to v8.3.9 (#1597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 18:03:08 -04:00
renovate[bot] a40021a823 Update actions/checkout action to v5 (#1582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:01:50 -04:00
renovate[bot] c86f7c5356 Update dependency com.android.tools.build:apksig to v8.12.1 (#1595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:01:37 -04:00
Constantin Piber 3075888d26 [#1575] Support copy & paste (#1593)
* [#1575] Support paste

* WebView: Implement copy

* Localize copy dialog, lint

* Implement a custom context menu for copy/paste

* Remove click event which causes double events

* WebView: Fix input events broken by moved preventDefault

We want to fall back to the `input` event for Android bug and paste, but
we want to prevent the event for the others, so change the order
2025-08-19 15:01:31 -04:00
Mitchell Syer 7a0d3a1efe Expose the Source baseUrl (#1585)
* Expose the source baseUrl

* Lint
2025-08-19 15:01:14 -04:00
renovate[bot] 7b22397a82 Update kotlin monorepo to v2.2.10 (#1589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:01:07 -04:00
Constantin Piber b2cfb5a1e9 AndroidCompat: Use NotoSans as default font (#1572)
* Initial Noto fonts

* Use Noto for other default fonts

* Typeface: Prefer main font

Eagerly switch back to main font as soon as it can display again;
otherwise we might never switch back (or later than necessary); we
should always prefer the main font

* fix: Font metrics with fallback font on TextLine
2025-08-19 15:00:59 -04:00
renovate[bot] 283e38c30a Update actions/download-artifact action to v5 (#1573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 15:00:43 -04:00
Constantin Piber 02717b317c [#1550] Allow page icons outside authentication (#1577) 2025-08-19 15:00:35 -04:00
Weblate (bot) 7fa1250a67 Weblate translations (#1580)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/de/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/es/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/vi/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Constantin Piber <cp.piber@gmail.com>
Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: zeedif <carlos_antonio-rl@hotmail.com>
2025-08-19 15:00:28 -04:00
Zeedif 590e43c827 feat(sync/koreader): Add KOReader reading progress synchronization (#1560)
* feat(sync/koreader): implement reading progress synchronization

This commit introduces a comprehensive integration with KOReader Sync Server to enable two-way synchronization of reading progress.

The core logic is encapsulated in a new `KoreaderSyncService`, which handles authentication, registration, and progress pushing/pulling based on user-defined strategies (LATEST, KOSYNC, SUWAYOMI).

Key changes include:

- A new GraphQL API is added to manage the KOReader Sync connection:
  - `connectKoSyncAccount` mutation provides a simplified flow that attempts to log in and, if the user doesn't exist, automatically registers them.
  - `logoutKoSyncAccount` mutation to clear credentials.
  - `koSyncStatus` query to check the current connection status.

- Reading progress is now synchronized at key points:
  - The `fetchChapterPages` mutation pulls the latest progress from the sync server before loading the reader. It respects the configured sync strategy and updates the local database if necessary.
  - The `updateChapters` and other progress-updating methods now push changes to the sync server automatically.
- OPDS chapter entries also pull the latest progress, ensuring clients receive up-to-date reading status.

- Supporting backend changes have been made:
  - The `Chapter` table is extended with a `koreader_hash` column to uniquely identify documents. A database migration is included.
  - New configuration options are added to `server.conf` to manage the feature (enable, server URL, credentials, strategy, etc.).

* perf(opds): defer KOReader sync to improve chapter feed performance

Removes the KOReader Sync progress-pulling logic from the `createChapterListEntry` function.

The previous implementation triggered a network request to the sync server for every single chapter when generating a list, leading to severe performance issues and slow load times on feeds with many entries.

This change reverts to the more performant approach of always linking to the chapter's metadata feed. The expensive sync operation will be handled within the metadata entry generation instead, ensuring it's only triggered on-demand for a single chapter. This restores the responsiveness of browsing chapter feeds.

* refactor(koreader): Use enums for sync settings and correct OPDS logic

Refactor Koreader Sync settings to use enums instead of raw strings for `checksumMethod` and `strategy`. This improves type safety, prevents typos, and makes the configuration handling more robust.

The changes include:
- Introducing `KoreaderSyncChecksumMethod` and `KoreaderSyncStrategy` enums.
- Updating `ServerConfig`, GraphQL types, and backup models to use these new enums.
- Refactoring `KoreaderSyncService` to work with the enum types.

Additionally, this commit fixes an issue in `OpdsEntryBuilder` where the logic for determining which sync progress to use (local vs. remote) was duplicated. The builder now correctly delegates this decision to `KoreaderSyncService.pullProgress`, which already contains the necessary strategy logic. This centralizes the logic and ensures consistent behavior.

* refactor(koreader): Improve config handling and remove redundant update

This commit combines several refactoring and cleanup tasks:

- **Koreader Sync:** The sync service is updated to use the modern `serverConfig` provider instead of the legacy `GlobalConfigManager`. This aligns it with the current configuration management approach in the project.

- **Download Provider:** A redundant `pageCount` database update is removed from `ChaptersFilesProvider`. This operation was unnecessary because the `getChapterDownloadReady` function, which is called earlier in the download process, already verifies and corrects the page count. This change eliminates a superfluous database write and fixes a related import issue.

* feat(sync/koreader)!: enhance sync strategy and add progress tolerance

This commit overhauls the KOReader synchronization feature to provide more granular control and robustness. The simple on/off toggle has been replaced with a more flexible strategy-based system.

Key changes include:
- Replaced `koreaderSyncEnabled` with a more powerful `koreaderSyncStrategy` enum.
- Introduced new sync strategies: `PROMPT`, `SILENT`, `SEND`, `RECEIVE`, and `DISABLE`, allowing for fine-grained control over the sync direction and conflict resolution.
- Added a `koreaderSyncPercentageTolerance` setting. This prevents unnecessary sync updates for minor progress differences between Suwayomi and KOReader.
- Refactored the `KoreaderSyncService` to implement the new strategies and use the configurable tolerance.
- Updated GraphQL schemas, mutations, and server configuration to remove the old setting and incorporate the new ones.
- Adjusted the backup and restore process to correctly handle the new configuration parameters.
- Modified API endpoints and internal logic to check and apply remote progress based on the selected strategy.

BREAKING CHANGE: The `koreaderSyncEnabled` setting is removed and replaced by a more granular `koreaderSyncStrategy`. The enum values for the strategy have been completely changed, making previous configurations incompatible.

* fix: remove unused imports

* feat(opds, sync): enhance Koreader sync and OPDS conflict handling

This commit introduces significant improvements to the Koreader synchronization feature, focusing on providing a better user experience for handling progress conflicts in both OPDS and GraphQL clients.

Key changes include:

- **OPDS Conflict Resolution:** When a reading progress conflict is detected, the OPDS feed for a chapter now provides two distinct "Read Online" links: one to continue from the local progress and another to continue from the synced progress from the remote device (e.g., "Continue Reading Online (Synced from KOReader)"). This empowers users to choose which progress to follow.

- **GraphQL Sync Conflict Information:** The `fetchChapterPages` GraphQL mutation now includes a `syncConflict` field in its payload. This field provides the remote device name and page number, allowing GraphQL clients to implement a user-facing prompt to resolve sync conflicts.

- **Improved Sync Strategy Handling:**
  - The `connectKoSyncAccount` mutation no longer unconditionally sets the sync strategy to `PROMPT`. It now respects the user's existing setting, preventing accidental configuration changes upon re-login.
  - The default `koreaderSyncStrategy` in the configuration is changed to `DISABLED`, providing a safer and more intuitive default for new users.

- **Refinements & Fixes:**
  - The fallback for the remote device name is now centralized within the KoreaderSyncService, defaulting to "KOReader" if not provided.
  - Renamed `KoreaderSyncStrategy.DISABLE` to `DISABLED` for consistency.
  - Updated i18n strings for OPDS links to be more descriptive and user-friendly.

* refactor(kosync): rename stream page link titles for consistency

* refactor(kosync): return SettingsType in auth mutation payloads

The `connectKoSyncAccount` and `logoutKoSyncAccount` mutations modify server settings (username and userkey) but did not previously return the updated configuration. This forced client applications to manually refetch settings to avoid a stale cache.

This change modifies the payloads for both mutations to include the full `SettingsType`.

By returning the updated settings directly, GraphQL clients like Apollo Client can automatically update their cache, simplifying client-side state management and ensuring the UI always reflects the current server configuration.

Additionally, `clientMutationId` has been added to `KoSyncConnectPayload` for consistency with GraphQL practices, aligning it with the logout mutation.

Refs: #1560

* refactor(kosync): replace KoSyncConnectPayload with ConnectResult in connect method

* fix(kosync): add koreaderSyncPercentageTolerance default setting
2025-08-19 15:00:19 -04:00
Mitchell Syer 16e9c0b19a Migrate to GradleUp Shadow (#1561) 2025-08-19 15:00:09 -04:00
renovate[bot] 8661438f69 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.13 (#1568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-19 14:59:59 -04:00
Mitchell Syer 9049b4a090 [skip ci] Weblate fixes (#1579) 2025-08-09 15:20:00 -04:00
Constantin Piber a2622fe3e1 [#1563] Return default if no matching locale found (#1576) 2025-08-07 21:35:19 -04:00
Constantin Piber 664d5fe637 Canvas: Implement drawColor for author notes (#1562) 2025-08-01 01:35:49 -04:00
renovate[bot] 97f9180063 Update xmlserialization to v0.91.2 (#1549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 19:55:52 -04:00
renovate[bot] eed8012521 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.10 (#1559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 19:55:41 -04:00
renovate[bot] ab1e3e4302 Update dependency com.android.tools.build:apksig to v8.12.0 (#1557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 19:55:35 -04:00
renovate[bot] c12b6f39d8 Update dependency org.apache.commons:commons-compress to v1.28.0 (#1555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 19:55:29 -04:00
renovate[bot] b280c03afa Update dependency com.squareup.okio:okio to v3.16.0 (#1554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-31 19:55:23 -04:00
schroda 1d9991e562 Feature/Improve chapter download with valid existing download handling (#1553)
* Fix early exit on download for existing download for FolderProvider

The current check only worked for the "ArchiveProvider". The "FolderProvider" never moved the existing download to the cache folder.

In case the existing download is considered to be reusable, there is no need to proceed with the download logic.

* Fix "ArchiveProvider#extractExistingDownload"

The "ChaptersFilesProvider#extractExistingDownload" expects the download to be extracted into the final download folder.
However, the "ArchiveProvider" extracted the download into the chapter download cache folder.

* Add chapter download function call requirements
2025-07-31 19:55:09 -04:00
Zeedif 87aae46a1f Overhaul OPDS feeds for discovery, filtering, and enhanced UX (#1543)
* fix: correct chapter facets URL to include /chapters endpoint

Update addChapterSortAndFilterFacets to use the correct URL path
from `/manga/{id}` to `/manga/{id}/chapters` for proper routing.

* feat(opds): restructure feeds and add exploration capabilities

This commit completely refactors the OPDS v1.2 implementation to align it more closely with the WebUI experience, separating "Library" browsing from "Explore" functionality.

Key changes include:

- The root feed is now a navigation feed directing to distinct "Library" and "Explore" sections.
- A new "History" feed has been added to the root to show recently read chapters.
- The "Explore" section now allows browsing all available sources, not just those with manga in the library.
- Feeds for exploring a source now support faceting by "Popular" and "Latest", mirroring the WebUI.
- The "Library" section retains all previous browsing methods (by category, genre, status, etc.).
- Facet link generation has been corrected to use the proper base URL, fixing broken navigation in chapter lists.
- The `OpdsFeedBuilder.kt` file has been refactorized into smaller, more manageable helper files (`OpdsEntryBuilder.kt`, `OpdsFeedHelper.kt`) to resolve a `java.lang.OutOfMemoryError: GC overhead limit exceeded` error during compilation.
- All OPDS-related strings (`strings.xml`) have been updated to reflect the new structure and improve clarity.

This new structure provides a much more intuitive and powerful browsing experience for OPDS clients, enabling content discovery in addition to library management.

* feat(opds)!: implement advanced filtering and sorting for library feeds

This commit significantly enhances the OPDS library feeds by introducing advanced sorting and filtering capabilities, mirroring the features available in the WebUI. It also standardizes the terminology from "manga" to "series" across all user-facing OPDS feeds for better clarity and consistency.

Key Features & Changes:

- **Library Facets:** All library feeds (All Series, By Source, By Category, By Genre, etc.) now include OPDS facets for:
  - **Sorting:** By title (A-Z, Z-A), last read, latest chapter, date added, and total unread chapters.
  - **Filtering:** By content status including unread, downloaded, ongoing, and completed.

- **Terminology Update:** The term "manga" has been replaced with "series" in all user-facing OPDS titles, descriptions, and endpoints to align with the frontend terminology.

- **Code Refactoring:**
  - `MangaRepository` has been updated with the correct Exposed SQL syntax (`Case`/`sum` for conditional counts, `having` clause for filtering on aggregates) to support the new facets.
  - `OpdsEntryBuilder` now includes a new function `addLibraryMangaSortAndFilterFacets` to generate the facet links.
  - `OpdsV1Controller` and `OpdsFeedBuilder` have been updated to handle the new `sort` and `filter` parameters and to call the new facet generation logic.

BREAKING CHANGE: The API endpoints for manga have been renamed to use 'series'. Any client implementation will need to update its routes.
For example, `/api/opds/v1.2/manga/{id}/chapters` is now `/api/opds/v1.2/series/{id}/chapters`.

* feat(opds): add item counts (thr:count) to navigation and facet links

This change enhances the OPDS feeds by including the number of items for various navigation links and filter facets, adhering to the OPDS 1.2 specification.

The `thr:count` attribute provides a hint to clients about the number of entries in a linked feed, significantly improving the user experience by showing counts upfront.

- Navigation Feeds (Categories, Sources, Genres, Statuses, Languages) now display the total number of manga for each entry in their respective links.
- Acquisition Feeds for the library and chapters now include counts for their filter facets (e.g., Unread, Downloaded, Completed).

This required updating DTOs to carry count data, modifying repository queries to calculate these counts efficiently, and adjusting the feed builders to include the `thr:count` attribute in the generated XML.

* refactor(opds)!: simplify root feed by removing library sub-level

The OPDS feed navigation was previously nested, requiring users to first select "Library" and then navigate to a subsection like "All Series" or "Categories". This extra step is cumbersome for OPDS clients and complicates the user experience.

This change elevates all library-related navigation entries directly to the root feed, flattening the hierarchy and making content more accessible.

As part of this refactoring:
- The `getLibraryFeed` builder and its corresponding controller/API endpoints have been removed.
- Unused string resources for the "Library" entry have been deleted.

BREAKING CHANGE: The `/api/opds/v1.2/library` endpoint has been removed. Clients should now discover library sections directly from the root feed at `/api/opds/v1.2`.

* feat(opds): enhance feeds with comprehensive manga and chapter details

This commit significantly enriches the OPDS feeds to provide a more detailed and compliant user experience.

- Refactored `OpdsMangaAcqEntry` and `OpdsChapterMetadataAcqEntry` to include additional fields such as status, source information, author, description, and web URLs.
- The OPDS entry builder (`OpdsEntryBuilder`) now populates entries with this richer metadata, including summaries, content descriptions, authors, and categories, aligning more closely with the OPDS Catalog specification.
- Added OPDS constants for 'popular' and 'new' sort relations to align with the specification.
- Included "alternate" links for both manga and chapters, allowing clients to open the item on its source website ("View on web").
- Updated internationalization strings and constants to support the new features and metadata.

* fix(opds): fetch chapters for non-library manga in feed

Previously, when accessing the OPDS chapter feed for a manga discovered via the "Explore" feature (and thus not yet in the library), the feed would be empty. This was because the feed generation logic only queried the local database, which had no chapter entries for these manga.

This commit resolves the issue by modifying `getSeriesChaptersFeed` to be a suspend function. It now implements a fallback mechanism:
- It first attempts to load chapters from the local database.
- If no chapters are found, it triggers an online fetch from the source to populate the database.
- It then re-queries the local data to build the complete chapter feed.

This ensures that chapter lists are correctly displayed for all manga, whether they are in the library or being explored for the first time.

Additionally, this commit includes a minor correction to the URN identifier for the root feed to better align with its path.

* feat(opds): provide direct stream and acquisition links when page count is known

Previously, the OPDS chapter feed always provided a single link to a separate metadata feed for each chapter. This was done to defer the costly operation of fetching the page count for undownloaded chapters, ensuring the main chapter list loaded quickly.

This commit introduces a more efficient, conditional approach. If a chapter's page count is already known (e.g., because it's downloaded or has been previously fetched), the chapter feed entry now includes direct links for:

-   OPDS-PSE page streaming (`pse:stream`).
-   CBZ file acquisition (`acquisition/open-access`).
-   Chapter cover image (`image`).

If the page count is not known, the entry falls back to the previous behavior, linking to the metadata feed to perform the page count lookup on-demand.

This significantly improves the user experience for OPDS clients by reducing the number of requests needed to start reading or downloading chapters that are already available on the server, making navigation faster and more fluid.

* fix(opds): resolve suspend calls and add missing lastReadAt for OPDS feeds

The OPDS feed generation was failing to compile due to two main issues:
1. The `OpdsChapterListAcqEntry` DTO was missing the `lastReadAt` property, which is required for the OPDS-PSE `lastReadDate` attribute.
2. Several functions in `OpdsFeedBuilder` were attempting to call the `suspend` function `createChapterListEntry` from a non-coroutine context.

This commit resolves these issues by:
- Adding the `lastReadAt` field to `OpdsChapterListAcqEntry` and populating it correctly from the database in the `ChapterRepository`.
- Refactoring `getHistoryFeed`, `getLibraryUpdatesFeed`, and `getSeriesChaptersFeed` in `OpdsFeedBuilder` to be `suspend` functions.
- Wrapping the entry creation logic in `withContext(Dispatchers.IO)` to provide the necessary coroutine scope for the suspend call and to perform the mapping on a background thread.

* refactor(opds): standardize library feed generation and enhance facets

This commit refactors the OPDS v1.2 feed generation logic to improve code structure, correctness, and feature capability.

The primary changes include:
- A new private `getLibraryFeed` helper function in `OpdsV1Controller` has been introduced to centralize and DRY up the logic for creating library-specific acquisition feeds.
- A new `OpdsMangaFilter` DTO now encapsulates all filtering, sorting, and pagination parameters, simplifying the controller handlers and making them more maintainable.
- URL generation for category, genre, status, and language feeds has been corrected. Links now correctly point to root-level paths (e.g., `/opds/v1.2/genre/{name}`) instead of being incorrectly nested under `/library/`.
- The OPDS facet system is enhanced with more specific facet groups and "All" links for a better user experience when clearing filters.

Associated changes:
- i18n strings in `strings.xml` have been reorganized with comments and new strings have been added to support the enhanced facet groups.
- The route for the publication status feed has been renamed from `/status/{id}` to `/statuses` for consistency.
- KDoc comments have been added and improved throughout the affected files for better code documentation.

* fix(opds): revert direct acquisition links in chapter feeds to improve performance

This reverts commit 33cdc0d534292760a3225cee18e274df542f0778.

The previous change introduced direct stream and download links in chapter list feeds when the page count was known. While convenient, this caused a significant performance degradation on feeds with many chapters, as it required checking for the existence of a CBZ file for every single entry.

This commit restores the original behavior where chapter list entries always link to a dedicated metadata feed. This approach defers expensive I/O operations until a user explicitly requests a single chapter's details, ensuring that chapter list feeds load quickly and efficiently. Direct acquisition and streaming links are now exclusively generated within the metadata feed.
2025-07-31 19:53:40 -04:00
Syer10 7e211ee9a1 Release v2.1.1867
CI Publish / Validate Gradle Wrapper (push) Failing after 2m5s
CI Publish / Build Jar (push) Has been skipped
CI Publish / jlink (linux-x64, ubuntu-latest) (push) Failing after 2s
CI Publish / jlink (macOS-arm64, macos-14) (push) Has been cancelled
CI Publish / jlink (macOS-x64, macos-13) (push) Has been cancelled
CI Publish / jlink (windows-x64, windows-latest) (push) Has been cancelled
CI Publish / Make linux-assets release (push) Has been cancelled
CI Publish / Make appimage release (push) Has been cancelled
CI Publish / Make debian-all release (push) Has been cancelled
CI Publish / Make linux-x64 release (push) Has been cancelled
CI Publish / Make macOS-arm64 release (push) Has been cancelled
CI Publish / Make macOS-x64 release (push) Has been cancelled
CI Publish / Make windows-x64 release (push) Has been cancelled
CI Publish / release (push) Has been cancelled
2025-07-31 13:56:03 -04:00
Mitchell Syer 8e8883ba37 Update electron (#1556) 2025-07-31 12:01:33 -04:00
schroda 02c4398e48 Fix handling of too long page image urls migration (#1552)
* Delete duplicated chapter page rows by index and chapter

In case duplicated rows based on the condition for the updated unique constraint existed, the new constraint could not be added and caused the migration to fail

* Drop UC_PAGE only if it exists
2025-07-29 18:00:10 -04:00
schroda ad7a8dd7dc Fix/page download conversion reduce logs (#1545)
* Cleanup chapter page conversion

* Reduce chapter page conversion logging
2025-07-25 19:42:06 -04:00
schroda e3338211d6 Handle too long page image urls (#1544)
Attempted fix of 3ff29aa38a might not work, because there is no guarantee that the extension supports retrieving a specific page.
2025-07-25 19:42:00 -04:00
Mitchell Syer 7cab4b9229 Simplify secondary config parse (#1540)
* Add backslash escaping

* Use parseMap instead
2025-07-21 22:20:58 -04:00
Mitchell Syer ac5f1a0d93 Add enabled preference setting (#1539)
* Add enabled preference setting

* Don't change preference if its not enabled
2025-07-21 15:13:17 -04:00
Mitchell Syer 798b9d0c98 Fix cookies when domain is null (#1538) 2025-07-21 15:13:04 -04:00
Chiru-Dey 3ff29aa38a snowmtl extension error fix: dynamic retrieval (#1531)
* dynamic retrieval

* ktlint errors fixed

* reinstated comments
2025-07-21 15:12:57 -04:00
renovate[bot] f8c2b9ffb0 Update dependency adoptium/temurin21-binaries to jdk-21.0.8+9 (#1529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 15:12:49 -04:00
Syer10 888bb8897a Update AppImageTool download location
Not very impressed by the random name change that breaks existing scripts, no warning given.
2025-07-20 17:26:15 -04:00
schroda 192136e66c Change "download conversion compression level" type to Double (#1535)
https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/writing-schemas/scalars/#primitive-types
2025-07-20 17:00:00 -04:00
schroda 5057a57f7f Properly bind track privately (#1534)
In case the track was read from the TrackSearchTable the private status was never applied
2025-07-20 16:59:48 -04:00
renovate[bot] d81a4e0b7f Update dependency io.mockk:mockk to v1.14.5 (#1527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-16 21:53:05 -04:00
Mitchell Syer c63a06730f Fix downloads on errors when converting image (#1526)
* Fix downloads on errors when converting image

* Lint

* Simplify it

* No need for return

* Simplified
2025-07-16 21:52:54 -04:00
Mitchell Syer bef326d2d7 Fix paths in system properties (#1528)
* Fix paths in system properties

* Remove uneeded parse

* Cleanup name
2025-07-16 21:52:34 -04:00
renovate[bot] b8e85422f0 Update polyglot to v24.2.2 (#1523)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 15:38:29 -04:00
Constantin Piber d050bfdc68 Localize WebView and Login pages (#1522)
* Localize WebView and Login pages

* Switch to JTE for page rendering

* Lint

* Add gradle task dependency

* JTE -> KTE

* ShouldRunAfter

* I guess we must

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-07-15 15:38:20 -04:00
schroda 3bac176bf6 Prevent UnsupportedOperationException in DownloadManager (#1521)
CopyOnWriteArraySet does not support the usage of "removeAll" with a predicate
2025-07-15 09:32:11 -04:00
Syer10 7c506a42ae Install libfuse2 when creating AppImage 2025-07-14 18:59:50 -04:00
KamaleiZestri 2c436e2027 Add AppImage bundle (#1519)
* Add AppImage build

* add appimage package variable name

* Add workflows

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-07-14 18:51:09 -04:00
Constantin Piber df0078b725 [#1496] Image conversion (#1505)
* [#1496] First conversion attempt

* [#1496] Configurable conversion

* Fix: allow nested configs (map)

* [#1496] Support explicit `none` conversion

* Use MimeUtils for provided download

* [1496] Support image conversion on load for downloaded images

* Lint

* [#1496] Support conversion on fresh download as well

Previous commit was only for already downloaded images, now also for
fresh and cached

* [#1496] Refactor: Move where conversion for download happens

* Rewrite config handling, improve custom types

* Lint

* Add format to pages mutation

* Lint

* Standardize url encode

* Lint

* Config: Allow additional conversion parameters

* Implement conversion quality parameter

* Lint

* Implement a conversion util to allow fallback readers

* Add downloadConversions to api and backup, fix updateValue issues

* Lint

* Minor cleanup

* Update libs.versions.toml

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-07-14 17:51:18 -04:00
schroda 09c950a890 Fix/gql download subscription errors spamming emits (#1518)
* Remove immediate download notification for latest gql subscription

There is a problem where too many immediate updates can cause the client to lag out (e.g., in case it has to update the queue in the cache based on the updates).
This happens in case e.g., a source is broken and all its downloads error out basically immediately.
With each errored out download, a new one starts, which causes an immediate notification to the clients.

* Determine downloader status from active state of downloader jobs

In case the downloader is active but all downloads are erroring out immediately, no download will have the DOWNLOADING status.
This then would result in the downloader status to constantly be STOPPED.

* Prevent multiple update for the same downloads

It was possible that multiple updates got added for the same download.
This caused issues with the graphql apollo client, because it wasn't able to correctly update the client cache.

* Set download error state only after reaching max retries

In case the max retries haven't been reached yet, the download will be retried and thus setting and emitting the error state will cause weird looking ui updates.
2025-07-14 17:50:03 -04:00
schroda e7e76ed68d Prevent duplicated meta entries in database (#1517)
fixes #1513
2025-07-14 17:49:27 -04:00
schroda 06c1eeb995 Add missing transaction context to manga category update (#1516)
fixes #1510
2025-07-14 17:49:03 -04:00
renovate[bot] d545d852c5 Update dependency com.android.tools.build:apksig to v8.11.1 (#1511)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 17:48:37 -04:00
renovate[bot] 3486e8dcf3 Update dependency com.typesafe:config to v1.4.4 (#1509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 17:48:27 -04:00
renovate[bot] 1956b700fc Update dependency com.github.usefulness:webp-imageio to v0.10.2 (#1507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 17:48:21 -04:00
renovate[bot] 64ad9af344 Update okhttp monorepo to v5.1.0 (#1506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 17:48:14 -04:00
renovate[bot] 55fec0b82c Update plugin ktlint to v13 (#1504)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 17:48:06 -04:00
Syer10 8323ef5f41 [skip ci] Winget now uses Suwayomi.Suwayomi-Server 2025-07-08 18:26:35 -04:00
Syer10 8dbab23de7 [skip ci] Manual release only 2025-07-07 23:21:29 -04:00
Syer10 825d232613 [skip ci] Manually input version 2025-07-07 23:14:51 -04:00
Syer10 d797a03502 Modify Winget 2025-07-07 23:05:33 -04:00
Mitchell Syer 0ef6d74514 Add auth to log protection (#1501) 2025-07-06 14:01:37 -04:00
Constantin Piber 6234e897a8 [#1497] WebView: Localstorage (#1500)
* [#1497] WebView: Localstorage

* WebView: Transition to our own header/postData system

This is also what is recommended by most other posts, I haven't seen the
context used anywhere, and `KCEFResourceRequestHandler` seems to just
bypass a lot of CEF

* Lint
2025-07-06 12:09:31 -04:00
renovate[bot] fe121f59b0 Update okhttp monorepo to v5.0.0 (#1493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 12:09:16 -04:00
renovate[bot] 90cf5fcdec Update dependency com.squareup.okio:okio to v3.15.0 (#1488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 12:09:00 -04:00
renovate[bot] 8b5782a5b6 Update dependency gradle to v8.14.3 (#1494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 12:08:44 -04:00
Constantin Piber 68a131dbeb [#1349] Basic Cookie Authentication (#1498)
* [#1349] Stub basic cookie authentication

* [#1349] Basic login page

Also adjusts WebView header color and shadow to match WebUI. WebUI uses
a background-image gradient to change the perceived color, which was not
noticed originally.

* [#1349] Handle login post

* [#1349] Redirect to previous URL

* [#1349] Return a basic 401 for api endpoints

Instead of redirecting to a visual login page, API should just indicate
the bad state

* Use more appropriate 303 redirect

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

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

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

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

* Lint

* Transition to AuthMode enum with migration path

* Make basicAuthEnabled auto property, Lint

* ConfigManager: Make sure to re-parse the config after migration

* basicAuth{Username,Password} -> auth{Username,Password}

* Lint

* Update server settings backup model

* Update comment

* Minor cleanup

* Improve backup legacy settings fix

* Lint

* Simplify config value migration

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-07-06 12:08:29 -04:00
Mitchell Syer 1411c02e18 [skip ci] Update CONTRIBUTING.md 2025-07-02 23:38:22 -04:00
Constantin Piber 81fe3f0108 Stop dumping cookies in the console (#1490) 2025-07-02 13:35:56 -04:00
Constantin Piber c15cf23168 Kcef: Disable SHM (#1489)
In Docker, `/dev/shm` is restricted, so Chromium dies of OOM

See also https://stackoverflow.com/a/56941767/7508309
2025-07-02 13:23:21 -04:00
Constantin Piber a79dc580a5 Browser Webview (#1486)
* WebView: Add initial controller

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

* WebView: Prepare page

* WebView: Basic HTML setup

* WebView: Improve navigation

* WebView: Refactor message class deserialization

* WebView: Refactor event message serialization

* WebView: Handle click events

* WebView: Fix events after refactor

* WebView: Fix normalizing of URLs

* WebView: HTML remove navigation buttons

* WebView: Handle more events

* WebView: Handle document change in events

* WebView: Refactor to send mutation events

* WebView: More mouse events

* WebView: Include bubbles, cancelable in event

Those seem to be important

* WebView: Attempt to support nested iframe

* WebView: Handle long titles

* WebView: Avoid setting invalid url

* WebView: Send mousemove

* WebView: Start switch to canvas-based render

* WebView: Send on every render

* WebView: Dynamic size

* WebView: Keyboard events

* WebView: Handle mouse events in CEF

This is important because JS can't click into iFrames, meaning the
previous solution doesn't work for captchas

* WebView: Cleanup

* WebView: Cleanup 2

* WebView: Document title

* WebView: Also send title on address change

* WebView: Load and flush cookies from store

* WebView: remove outdated TODOs

* Offline WebView: Load cookies from store

* Cleanup

* Add KcefCookieManager, need to figure out how to inject it

* ktLintFormat

* Fix a few cookie bugs

* Fix Webview on Windows

* Minor cleanup

* WebView: Remove /tmp image write, lint

* Remove custom cookie manager

* Multiple cookie fixes

* Minor fix

* Minor cleanup and add support for MacOS meta key

* Get enter working

* WebView HTML: Make responsive for mobile pages

* WebView: Translate touch events to mouse scroll

* WebView: Overlay an actual input to allow typing on mobile

Browsers will only show the keyboard if an input is focused. This also
removes the `tabstop` hack.

* WebView: Protect against occasional NullPointerException

* WebView: Use float for clientX/Y

* WebView: Fix ChromeAndroid being a pain

* Simplify enter fix

* NetworkHelper: Fix cache

* Improve CookieStore url matching, fix another cookie conversion issue

* Move distinctBy

* WebView: Mouse direction toggle

* Remove accidentally copied comment

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-07-01 17:28:41 -04:00
renovate[bot] 8a62c6295d Update moko to v0.25.0 (#1487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 17:28:28 -04:00
renovate[bot] 88e77e1547 Update okhttp monorepo to v5.0.0-alpha.17 (#1485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 17:28:06 -04:00
Weblate (bot) 534619bc1a Weblate translations (#1484)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pt/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: 9811pc <9811.main@gmail.com>
Co-authored-by: N'Num Yutthaphon Inchaiya <yutthaphon30667@gmail.com>
Co-authored-by: Psico <psikenji@users.noreply.hosted.weblate.org>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
2025-07-01 17:27:56 -04:00
Constantin Piber ae904753f7 systemd: use startup script, X server (#1482)
* systemd: use startup script

* script: Start X server using `xvfb-run` if DISPLAY is not set
2025-06-28 17:04:13 -04:00
Mitchell Syer 8c4a2cb529 Add chapter lastReadAt to backups as BackupHistory (#1477)
* Add chapter lastReadAt to backups as BackupHistory

* MaxOrNull
2025-06-28 17:04:04 -04:00
renovate[bot] 16d4893480 Update dependency com.squareup.okio:okio to v3.14.0 (#1483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:55 -04:00
renovate[bot] a7446e2f4c Update dependency com.github.usefulness:webp-imageio to v0.10.1 (#1480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:47 -04:00
renovate[bot] 9dcae193a9 Update serialization to v1.9.0 (#1479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:36 -04:00
renovate[bot] 36fac0f3f4 Update dependency com.android.tools.build:apksig to v8.11.0 (#1473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:26 -04:00
renovate[bot] 24a4c176c0 Update plugin buildconfig to v5.6.7 (#1469)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:17 -04:00
renovate[bot] 9c7f50e91e Update kotlin monorepo to v2.2.0 (#1466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:03:07 -04:00
renovate[bot] e52bc255f3 Update dependency org.jsoup:jsoup to v1.21.1 (#1465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:02:58 -04:00
Weblate (bot) ae4c9887d8 Translations update from Hosted Weblate (#1471)
* Weblate translations

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: N'Num Yutthaphon Inchaiya <yutthaphon30667@gmail.com>
Co-authored-by: Psico <psikenji@users.noreply.hosted.weblate.org>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pt/
Translation: Suwayomi/Suwayomi-Server

* Deleted translation using Weblate (Thai)

* Deleted translation using Weblate (Portuguese (Portugal))

* Update languages.json

---------

Co-authored-by: N'Num Yutthaphon Inchaiya <yutthaphon30667@gmail.com>
Co-authored-by: Psico <psikenji@users.noreply.hosted.weblate.org>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-06-28 17:02:46 -04:00
Mitchell Syer 52201e2488 Add private to trackrecords filter (#1468)
* Add private to trackrecords filter

* Remove sourceMapping from base

* Format
2025-06-26 22:04:26 -04:00
renovate[bot] c3e2b0e002 Update dependency io.mockk:mockk to v1.14.4 (#1464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 20:19:08 -04:00
renovate[bot] 709915bf59 Update dependency io.javalin:javalin to v6.7.0 (#1460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 20:17:52 -04:00
renovate[bot] 9d7ec6fd60 Update dependency io.mockk:mockk to v1.14.3 (#1462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 20:17:46 -04:00
Mitchell Syer ee9de376a3 Fix new private parameter in tracking backup (#1463)
* Fix new private parameter in tracking backup

* Cleanup uneeded models
2025-06-22 20:17:31 -04:00
Mitchell Syer b54dc6f967 Fix Tracking DisplayScore (#1461) 2025-06-22 15:53:25 -04:00
Mitchell Syer abea85d831 Update Tracking Backend (#1457)
* Update Tracking Library

* Update Bangumi

* Update Anilist

* Update MangaUpdates

* Update MAL

* Add private to bind track

* Use null

* Remove old nullable

* Remove custom implementation of supportsTrackDeletion

* Add private to updateTrack

* Some descriptions

* Another description
2025-06-22 10:38:22 -04:00
Constantin Piber 972137c035 Paint: Support Typeface (#1459)
* Paint: Support Typeface

* Paint: Undo textSize transform
2025-06-22 10:38:09 -04:00
Mitchell Syer 1cdef5e0ee Use Kotlin AppDirs (#1453) 2025-06-21 12:02:14 -04:00
Constantin Piber bd7ea64b02 Add an abort handler and preload it on Linux (#1456)
Some native code (CEF) may cause SIGTRAP to be sent on fatal errors,
which brings down the entire server. Instead only kill the thread and
attempt to continue.
2025-06-21 12:02:05 -04:00
Constantin Piber 20c850c10b Implement Bitmap.copy, text layouting (#1455)
* Bitmap: Use provided config

* Bitmap: implement copy

* Bitmap: Simplify getPixels

This also fixes a bug where the returned data may not be in the correct
format

Android getPixels():
> The returned colors are non-premultiplied ARGB values in the sRGB color space.
BufferedImage getRGB():
> Returns an array of integer pixels in the default RGB color model (TYPE_INT_ARGB) and default sRGB color space

* Stub TextPaint and Paint

* Paint: Implement some required functions

* Stub StaticLayout and Layout

* Implement some Paint support

* Draw Bounds

* WebP write support

* First text rendering

* Paint: Fix text size, font metrics

* Paint: Fix not copying new properties

Fixes font size in draw

* Canvas: Stroke add cap/join for better aliasing

Otherwise we get bad artifacts on sharp corners

Based on https://stackoverflow.com/a/35222059/

* Remove logs

* Canvas: Implement other drawText methods

* Bitmap: support erase

* Layout: Fix text direction

Should be LTR, otherwise 0 is read, which is automatically interpreted
as RTL without explicit check

* Bitmap: scale to destination rectangle

* Canvas: drawBitmap with just x/y

* Bitmap: Convert image on JPEG export to RGB

JPEG does not support alpha, so will throw "bogus color space"

* Switch to newer fork
2025-06-21 12:01:56 -04:00
Constantin Piber 0b021e6c42 Increase WebView compatibility (#1451)
* LoadData: Use regular load but intercept request

The method we used before, `createBrowserWithHtml`, is implemented by
KCEF. This method creates a `file://` url and adds handlers for that.
Instead, use regular `createBrowser` and intercept the request later on.
This has the effect of creating the page with the correct origin, while
still setting the requested HTML instead of live data. This is important
for scripts due to CORS.

Also fixes a mistake in the ResourceRequestHandler, where (a) the status
was not set, resulting in ABORT, (b) the return value of `readResponse`
was correct (`false` too early) and (c) the callback was unnecessarily
called on the MainLoop.
Based on https://stackoverflow.com/a/52423252/

* Convince the compiler we're doing it right

Invoking "public final" methods would fail. Not sure why this only
happens for some extensions, but it does. We need to tell the compiler
we're sure we have access to it, for some reason...

* JS: Invoke result handler on the loop

Some extensions call WebView methods on the result, so this should be on
the same loop as the WebView itself

* JS: Await arguments

* Fix using wrong URL property for errors
2025-06-20 12:21:25 -04:00
renovate[bot] 0d109cdd4f Update graphqlkotlin to v8.8.1 (#1450)
* Update graphqlkotlin to v8.8.1

* Remove manual graphql-java-core update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-06-20 12:21:13 -04:00
Syer10 1dab9e1a7d Fix database migration 2025-06-15 17:30:24 -04:00
Syer10 593b01819d Update locales 2025-06-15 17:16:51 -04:00
renovate[bot] 029aa9c01b Update plugin buildconfig to v5.6.6 (#1449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 17:15:26 -04:00
schroda 149b549d8d Handle chapter marked as downloaded without downloaded files (#1448)
In case a chapter was marked as downloaded but did not have any downloaded files, an uncaught exception was thrown
2025-06-15 17:15:10 -04:00
renovate[bot] 786635010e Update dependency com.squareup.okio:okio to v3.13.0 (#1447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 17:14:57 -04:00
Weblate (bot) d7fe170067 Weblate translations (#1445)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/de/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ta/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/zh_Hans/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Constantin Piber <cp.piber@gmail.com>
Co-authored-by: Poesty Li <poesty7450@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-06-15 17:14:44 -04:00
renovate[bot] e3d4be9a5a Update dependency org.bouncycastle:bcprov-jdk18on to v1.81 (#1443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 17:14:29 -04:00
schroda 4086a73727 Feature/backup suwayomi data (#1430)
* Export meta data

* Import meta data

* Add missing "opdsUseBinaryFileSize" setting to gql

* Export server settings

* Import server settings

* Streamline server config enum handling

* Use "restore amount" in backup import progress
2025-06-15 17:14:13 -04:00
schroda 483e3a760f Increase chapter scanlator column max length (#1425) 2025-06-15 17:13:57 -04:00
renovate[bot] 2757f881dc Update plugin ktlint to v12.3.0 (#1403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 17:13:39 -04:00
renovate[bot] e2aff0ece7 Update dependency com.pinterest.ktlint:ktlint-cli to v1.6.0 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 17:13:26 -04:00
Mitchell Syer 327526330f [skip ci] Update README.md 2025-06-12 11:59:41 -04:00
renovate[bot] ea976a4d0f Update dependency io.insert-koin:koin-core to v4.1.0 (#1442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:51:48 -04:00
renovate[bot] 728ada5e70 Update okhttp monorepo to v5.0.0-alpha.16 (#1441)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:51:35 -04:00
renovate[bot] c091ac4d67 Update xmlserialization to v0.91.1 (#1400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:50:52 -04:00
schroda ee4c852f1b Always update manga thumbnail on fetch (#1429)
It's possible that the cover changed, but the url is still the same.
In that case the cover never gets updated unless the downloaded/cached file gets deleted
2025-06-12 11:50:18 -04:00
schroda 1a5d334f6c Delete thumbnails during backup import (#1428)
Was accidentally removed with 633ea97848
2025-06-12 11:49:34 -04:00
schroda 7d72ff3514 Fix extracting "startDate" (#1427)
Value of json object was never properly accessed.
Start date can have different formats (yyyy-MM, yyyy-MM-dd) so it's not feasible to format it
2025-06-12 11:49:14 -04:00
schroda e224e91100 Dequeue downloads of removed chapters (#1426)
Otherwise, graphql errors will be caused because the chapters won't be found and are unexpectedly null.

fixes #1358
2025-06-12 11:48:27 -04:00
schroda 7c5edd1b73 Realign chapter number recognition with mihon (#1424)
https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383
2025-06-12 11:47:30 -04:00
Weblate (bot) 2621415f7c Weblate translations (#1422)
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/es/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pt/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/vi/
Translation: Suwayomi/Suwayomi-Server

Co-authored-by: Nguyễn Trung Đức <vaicato16@gmail.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: Zereef <rafael.v.veloso@proton.me>
Co-authored-by: marimo <nekomiminimoe@gmail.com>
2025-06-12 11:46:39 -04:00
renovate[bot] a3184c46b6 Update dependency com.android.tools.build:apksig to v8.10.1 (#1419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:45:36 -04:00
renovate[bot] 1575ffa6ae Migrate config renovate.json (#1420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:45:24 -04:00
renovate[bot] ee27da3de6 Update dependency com.github.Suwayomi:exposed-migrations to v3.8.0 (#1421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:45:12 -04:00
renovate[bot] 83a7224f2d Update exposed to v0.61.0 (#1291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:44:58 -04:00
schroda ecea2ecdf5 Fix/initial scheduling of global update (#1416)
* Fix setting initial global update delay

In case no update has run yet, and the "last automated update" defaulted to 0, the calculation always resulted in a multiple of the interval.

This resulted for e.g., interval 24, to always be scheduled at 00:00.
For e.g., interval 6, it was always one of the following times: 00:00, 06:00, 12:00, 18:00; depending on the current system time.

* Delete the existing "last automated update time"

So that the update will be triggered based on the time the server got started again after the update.

* Extract migrations into separate functions

* Cleanup migration execution logic
2025-06-12 11:44:38 -04:00
renovate[bot] f04060b31b Update graphqlkotlin to v8.8.0 (#1365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:43:56 -04:00
renovate[bot] d411d1966a Update dependency com.graphql-java:graphql-java to v22.4 (#1401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:43:25 -04:00
AwkwardPeak7 507bf07104 implement setSummaryOn and setSummaryOff in TwoStatePreference (#1431) 2025-06-12 11:41:31 -04:00
Constantin Piber 09061a38bc Negation missing in SystemProperties (#1433) 2025-06-12 11:41:08 -04:00
renovate[bot] 611a7db2e1 Update dependency gradle to v8.14.2 (#1435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:40:46 -04:00
David Brochero a5cf428ce5 doc: Add Neko integration instructions (#1440)
* doc: Add Neko integration instructions

* Update README.md

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2025-06-12 11:40:30 -04:00
renovate[bot] 31f06a2d43 Update dependency com.squareup.okio:okio to v3.12.0 (#1418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 11:39:38 -04:00
schroda 1d7a60b630 Automatically truncate required varchar columns (#1423) 2025-06-12 11:39:19 -04:00
Constantin Piber a2fadbe513 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>
2025-06-12 11:38:54 -04:00
Mitchell Syer dee61e191c [ci skip] Add Translation into to README 2025-05-27 16:26:23 -04:00
renovate[bot] 32b6461c6a Update dependency io.javalin:javalin to v6.6.0 (#1364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 20:46:48 -04:00
renovate[bot] 93fff42693 Update dependency gradle to v8.14.1 (#1398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 20:46:35 -04:00
Zeedif 61f429896c feat(opds): implement full internationalization and refactor feed gen… (#1405)
* feat(opds): implement full internationalization and refactor feed generation

This commit introduces a comprehensive internationalization (i18n) framework
and significantly refactors the OPDS v1.2 implementation for improved
robustness, spec compliance, and localization.

Key changes:

Internationalization (`i18n`):
- Introduces `LocalizationService` to manage translations:
    - Loads localized strings from JSON files (e.g., `en.json`, `es.json`)
      stored in a new `i18n` data directory.
    - Default `en.json` and `es.json` files are bundled and copied from
      resources on first run if not present.
    - Supports template resolution with `$t()` cross-references, locale
      fallbacks (to "en" by default), and argument interpolation ({{placeholder}}).
- `ServerSetup` now initializes the `i18n` directory and `LocalizationService`.

OPDS Refactor & Enhancements:
- Replaces the previous `Opds.kt` and `OpdsDataClass.kt` with a new
  `OpdsFeedBuilder.kt` and a set of more granular, spec-aligned XML
  models (e.g., `OpdsFeedXml`, `OpdsEntryXml`, `OpdsLinkXml`).
- Integrates `LocalizationService` throughout all OPDS feeds:
    - All user-facing text (feed titles, entry titles, summaries,
      link titles, facet labels for sorting/filtering) is now localized.
    - Adds a `lang` query parameter to all OPDS endpoints to allow
      clients to request a specific UI language.
    - Uses the `Accept-Language` header as a fallback for language detection.
- The OpenSearch description (`/search` endpoint) is now localized and
  its template URL includes the determined language.
- Centralizes OPDS constants (namespaces, link relations, media types)
  in `OpdsConstants.kt`.
- Adds utility classes `OpdsDateUtil.kt`, `OpdsStringUtil.kt`, and
  `OpdsXmlUtil.kt` for common OPDS tasks.
- `MangaDataClass` now includes `sourceLang` to provide the content
  language of the manga in OPDS entries (`<dc:language>`).
- Updates OpenAPI documentation for OPDS endpoints with more detail
  and includes the new `lang` parameter.

Configuration:
- Adds `useBinaryFileSizes` server configuration option. File sizes in
  OPDS feeds now respect this setting (e.g., MiB vs MB), utilized via
  `OpdsStringUtil.formatFileSizeForOpds`.

This major refactor addresses the request for internationalization
originally mentioned in PR #1257 ("it would be great if messages were
adapted based on the user's language settings"). It builds upon the
foundational OPDS work in #1257 and subsequent enhancements in #1262,
#1263, #1278, and #1392, providing a more stable and extensible
OPDS implementation. Features like localized facet titles from #1392
are now fully integrated with the i18n system.

This resolves long-standing requests for better OPDS support (e.g., issue #769)
by making feeds more user-friendly, accessible, and standards-compliant,
also improving the robustness of features requested in #1390 (resolved by #1392)
and addressing underlying data needs for issues like #1265 (related to #1277, #1278).

* fix(opds): revert MIME type to application/xml for browser compatibility

* fix(opds): use chapter index for metadata feed and correct link relation

- Change `getChapterMetadataFeed` to use `chapterIndexFromPath` (sourceOrder)
  instead of `chapterIdFromPath` for fetching chapter data, ensuring
  consistency with how chapters are identified in manga feeds.
- Add error handling for cases where manga or chapter by index is not found.
- Correct OPDS link relation for chapter detail/fetch link in non-metadata
  chapter entries from `alternate` to `subsection` as per OPDS spec
  for navigation to more specific content or views.

* Use Moko-Resources

* Format

* Forgot the Languages.json

* refactor(opds)!: restructure OPDS feeds and introduce data repositories

This commit significantly refactors the OPDS v1.2 implementation by introducing dedicated repository classes for data fetching and by restructuring the feed generation logic for clarity and maintainability. The `chapterId` path parameter for chapter metadata feeds has been changed to `chapterIndex` (sourceOrder) to align with how chapters are identified in manga feeds.

BREAKING CHANGE: The OPDS endpoint for chapter metadata has changed from `/api/opds/v1.2/manga/{mangaId}/chapter/{chapterId}/fetch` to `/api/opds/v1.2/manga/{mangaId}/chapter/{chapterIndex}/fetch`. Clients will need to update to use the chapter's source order (index) instead of its database ID.

Key changes:
- Introduced `MangaRepository`, `ChapterRepository`, and `NavigationRepository` to encapsulate database queries and data transformation logic for OPDS feeds.
- Moved data fetching logic from `OpdsFeedBuilder` to these new repositories.
- `OpdsFeedBuilder` now primarily focuses on constructing the XML feed structure using DTOs provided by the repositories.
- Renamed `OpdsMangaAcqEntry.thumbnailUrl` to `rawThumbnailUrl` for clarity.
- Added various DTOs (e.g., `OpdsRootNavEntry`, `OpdsMangaDetails`, `OpdsChapterListAcqEntry`) to define clear data contracts between repositories and the feed builder.
- Simplified `OpdsV1Controller` by reorganizing feed endpoints into logical groups (Main Navigation, Filtered Acquisition, Item-Specific).
- Updated `OpdsAPI` to reflect the path parameter change for chapter metadata (`chapterIndex` instead of `chapterId`).
- Added `slugify()` utility to `OpdsStringUtil` for creating URL-friendly genre IDs.
- Standardized localization keys for root feed entry descriptions to use `*.entryContent` instead of `*.description`.
- Added `server.generated.BuildConfig` (likely from build process).

* style(opds): apply ktlint fixes

* Delete server/bin

* refactor(i18n): remove custom LocalizationService initialization

* refactor(i18n): remove unused imports from ServerSetup

* refactor(model): remove sourceLang from MangaDataClass

* refactor(opds): rename OPDS binary file size config property

- Rename `useBinaryFileSizes` to `opdsUseBinaryFileSizes` in code and config
- Update related condition check in formatFileSizeForOpds

BREAKING CHANGE: Existing server configurations using `server.useBinaryFileSizes` need to migrate to `server.opdsUseBinaryFileSizes`

* refactor(opds): improve OPDS endpoint structure and documentation

- Restructure endpoint paths for better resource hierarchy
- Add descriptive comments for each feed type and purpose
- Rename `/fetch` endpoint to `/metadata` for clarity
- Standardize feed naming conventions in route definitions

BREAKING CHANGE: Existing OPDS client integrations using old endpoint paths (`/manga/{mangaId}` and `/chapter/{chapterIndex}/fetch`) require updates to new paths (`/manga/{mangaId}/chapters` and `/chapter/{chapterIndex}/metadata`)

* fix(opds): Apply review suggestions for localization and comments

* Fix

* fix(opds): Update chapter links to include 'chapters' and 'metadata' in URLs

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
2025-05-26 20:46:14 -04:00
schroda a9e03837a3 Exclude web manifest file from requiring authentication (#1414) 2025-05-26 20:46:02 -04:00
schroda 89421946af Properly deschedule active tasks (#1413) 2025-05-26 20:45:48 -04:00
Mitchell Syer 218af8ea54 Update Gradle Wrapper Validation (#1412)
* Update Gradle Wrapper Validation

* Use v4
2025-05-26 20:45:31 -04:00
Edge At Zero d0f79ca473 Fix: Validate zipEntry directories during extension asset decompression (#1407) 2025-05-26 20:45:20 -04:00
Mitchell Syer ec870759cf Add highest numbered chapter function in MangaType (#1397)
* Add highest numbered chapter function in MangaType

* Fix name
2025-05-22 19:58:09 -04:00
Shirish 0405a535c7 Feat: Adds OPDS Chapter Filtering/Ordering (#1392)
* Adds server level configs for OPDS

* PR comments

* Refactor server-reference.conf (itemsPerPage range)

* Coerce itemsPerPage (10, 5000) and default invalid sort orders to DESC

* Coerce itemsPerPage (10, 5000) and default invalid sort orders to DESC

* Change opdsChapterSortOrder type to Enum(SortOrder)

* Fix serialization of SortOrderEnum & Add `opdsShowOnlyDownloadedChapters` config
2025-05-22 19:57:55 -04:00
renovate[bot] 814e4ba744 Update kotlin monorepo to v2.1.21 (#1383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 19:57:38 -04:00
renovate[bot] 5621c1ab58 Update dependency com.android.tools.build:apksig to v8.10.0 (#1376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 19:57:25 -04:00
renovate[bot] f1fd8bc446 Update dependency io.mockk:mockk to v1.14.2 (#1371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 19:57:11 -04:00
renovate[bot] 60fdd6cda9 Update dependency org.jsoup:jsoup to v1.20.1 (#1369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 19:56:57 -04:00
renovate[bot] 3332363a10 Update plugin buildconfig to v5.6.5 (#1368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 19:56:44 -04:00
Mitchell Syer 538bd3f126 Improve Downloads Handling (#1387)
* Improve Downloads Handling

* Update known pagecount for downloaded chapters

* Get fresh data for downloadReady

* Format

* Assume downloaded if first page is found

* Filter out ComicInfoFile
2025-05-16 15:57:53 -04:00
Mitchell Syer 336f985894 Fix Downloaded pages with no cached pages from source (#1386) 2025-05-16 12:45:39 -04:00
BrutuZ ba6687355e Ignore hidden folders/archives for Local Source chapter list (#1377) 2025-05-16 12:45:31 -04:00
Mitchell Syer 983980d8da Add Alternatives to deb package (#1375) 2025-05-06 16:39:43 -04:00
renovate[bot] 82d4a401fd Update dependency gradle to v8.14 (#1363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:41:21 -04:00
renovate[bot] 76e9f42734 Update dependency com.squareup.okio:okio to v3.11.0 (#1362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:37:00 -04:00
renovate[bot] 0c0035370a Update polyglot to v24.2.1 (#1360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:34:24 -04:00
renovate[bot] a3ac136b3b Update dependency adoptium/temurin21-binaries to jdk-21.0.7+6 (#1359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:34:11 -04:00
renovate[bot] ed1509b54f Update dependency io.mockk:mockk to v1.14.0 (#1341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:33:58 -04:00
renovate[bot] 1d0dcd097c Update kotlinx-coroutines monorepo to v1.10.2 (#1337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 19:33:45 -04:00
schroda 785c0469ac Fix/m0045 prevent duplicated chapter pages migration (#1361)
* Fix "imageUrl" column name in migration

* Rename column "imageUrl" to "IMAGE_URL" of table "Page"
2025-04-27 19:33:15 -04:00
renovate[bot] 7594ae5fa5 Update xmlserialization to v0.91.0 (#1331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:56:42 -04:00
renovate[bot] 6b4e08fdd1 Update serialization to v1.8.1 (#1330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:56:19 -04:00
renovate[bot] 65435341f3 Update dependency io.github.oshai:kotlin-logging-jvm to v7.0.7 (#1329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:56:12 -04:00
renovate[bot] 9bc9f963b7 Update dependency io.insert-koin:koin-core to v4.0.4 (#1326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:55:58 -04:00
renovate[bot] a27501371f Update plugin buildconfig to v5.6.3 (#1322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:55:43 -04:00
renovate[bot] 9fafebc8e7 Update dependency com.android.tools.build:apksig to v8.9.2 (#1321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 18:55:07 -04:00
schroda 59d2151c92 Prevent duplicated chapter pages (#1353)
In case "ChapterForDownload#asDownloadReady" was called in quick succession, the page list got inserted twice.

This caused problems with getting the images from the rest endpoint, because they are selected by sorting them by asc index and selecting the page by using the provided index as an offset.

This, however, only works as long as there are no duplicates, otherwise, page indexes 1, 2; 3, 4; 5, 6; ... will just return the same page.
2025-04-27 18:54:54 -04:00
schroda 1cc2a05f90 [ci skip] Update outdated install instructions in README (#1356)
* Update outdated install instructions in README

* Update README.md
2025-04-27 18:15:38 -04:00
schroda 8aea6f5473 [ci skip] Update feature list in README (#1355) 2025-04-27 18:14:53 -04:00
456 changed files with 50880 additions and 7258 deletions
Vendored
BIN
View File
Binary file not shown.
+3
View File
@@ -9,3 +9,6 @@ ij_kotlin_name_count_to_use_star_import_for_members=2147483647
ktlint_standard_discouraged-comment-location=disabled ktlint_standard_discouraged-comment-location=disabled
ktlint_standard_if-else-wrapping=disabled ktlint_standard_if-else-wrapping=disabled
ktlint_standard_no-consecutive-comments=disabled ktlint_standard_no-consecutive-comments=disabled
[**/generated/**]
ktlint=disabled
+12 -1
View File
@@ -42,7 +42,18 @@ body:
label: Suwayomi-Server version label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**. description: You can find your Suwayomi-Server version in **More → About**.
placeholder: | placeholder: |
Example: "v2.0.1727" Example: "v2.2.2100"
validations:
required: true
- type: dropdown
id: database
attributes:
label: Used database
description: H2 is the default database
options:
- H2
- PostgreSQL
validations: validations:
required: true required: true
+6
View File
@@ -0,0 +1,6 @@
<!--
Pull Request Checklist:
- Mention what the pull request does and the reasons behind the changes
- Mention all issues the pull request is closing
- Make sure to update the CHANGELOG accordingly if necessary based on the LAST stable release
-->
+76 -9
View File
@@ -14,32 +14,44 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3 uses: gradle/actions/wrapper-validation@v5
build: build:
name: Build pull request name: Build pull request
needs: check_wrapper needs: check_wrapper
runs-on: ubuntu-latest runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps: steps:
- name: Checkout pull request - name: Checkout pull request
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 25
distribution: 'temurin' distribution: 'zulu'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
@@ -47,7 +59,62 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build Jar - name: Build And Run Jar
working-directory: master working-directory: master
run: ./gradlew ktlintCheck :server:shadowJar --stacktrace run: |
./gradlew ktlintCheck :server:shadowJar --stacktrace
gcc -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
JAR=$(ls ./server/build/*.jar| head -1)
set +e
timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
-Dsuwayomi.tachidesk.config.server.databaseUrl=postgresql://localhost:5432/postgres \
-Dsuwayomi.tachidesk.config.server.databaseUsername=postgres \
-Dsuwayomi.tachidesk.config.server.databasePassword=postgres \
-jar "$JAR"
ecode="$?"
# if exit code is not 124 or 125, then error
if ! [ "$ecode" -eq 124 -o "$ecode" -eq 125 ]; then
printf "exit code '%s' - exiting.\n" "$ecode"
exit "$ecode"
fi
timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-jar "$JAR"
ecode="$?"
# if exit code is not 124 or 125, then error
if ! [ "$ecode" -eq 124 -o "$ecode" -eq 125 ]; then
printf "exit code '%s' - exiting.\n" "$ecode"
exit "$ecode"
fi
exit 0
check_docs:
name: Validate that all options are documented
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v6
- name: Validate all options are documented
run: |
f="`cat ./server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt |
awk -F' ' 'BEGIN{prev=""}{if ($3 ~ "Mutable" && !(prev ~ "@Deprecated")) print "server." substr($2, 1, length($2)-1); prev=$0}' |
while read -r setting; do
if ! grep "$setting" ./docs/Configuring-SuwayomiServer.md >/dev/null; then
echo "Setting $setting not documented" >&2
echo ":"
fi
done`"
if [ -n "$f" ]; then
exit 1
fi
+59 -46
View File
@@ -15,10 +15,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3 uses: gradle/actions/wrapper-validation@v5
build: build:
name: Build Jar name: Build Jar
@@ -26,20 +26,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout master branch - name: Checkout master branch
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: master ref: master
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 25
distribution: 'temurin' distribution: 'zulu'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
@@ -54,14 +54,14 @@ jobs:
run: ./gradlew :server:shadowJar --stacktrace run: ./gradlew :server:shadowJar --stacktrace
- name: Upload Jar - name: Upload Jar
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: jar name: jar
path: master/server/build/*.jar path: master/server/build/*.jar
if-no-files-found: error if-no-files-found: error
- name: Upload icons - name: Upload icons
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: icon name: icon
path: master/server/src/main/resources/icon path: master/server/src/main/resources/icon
@@ -71,7 +71,7 @@ jobs:
run: tar -cvzf scripts.tar.gz -C master/ scripts/ run: tar -cvzf scripts.tar.gz -C master/ scripts/
- name: Upload scripts.tar.gz - name: Upload scripts.tar.gz
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: scripts name: scripts
path: scripts.tar.gz path: scripts.tar.gz
@@ -86,24 +86,24 @@ jobs:
name: linux-x64 name: linux-x64
- os: windows-latest - os: windows-latest
name: windows-x64 name: windows-x64
- os: macos-14 - os: macos-15
name: macOS-arm64 name: macOS-arm64
- os: macos-13 - os: macos-15-intel
name: macOS-x64 name: macOS-x64
os: [ubuntu-latest, windows-latest, macos-14, macos-13] os: [ubuntu-latest, windows-latest, macos-15, macos-15-intel]
steps: steps:
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 25
distribution: 'temurin' distribution: 'zulu'
- name: Package JDK - name: Package JDK
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2 run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JRE package - name: Upload JRE package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.name }}-jre name: ${{ matrix.name }}-jre
path: suwa path: suwa
@@ -112,52 +112,61 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: include:
- debian-all - name: debian-all
- linux-assets jre: linux-x64
- linux-x64 - name: appimage
- macOS-x64 jre: linux-x64
- macOS-arm64 - name: linux-assets
- windows-x64 jre: linux-assets
- name: linux-x64
jre: linux-x64
- name: macOS-x64
jre: macOS-x64
- name: macOS-arm64
jre: macOS-arm64
- name: windows-x64
jre: windows-x64
name: [debian-all, appimage, linux-assets, linux-x64, macOS-x64, macOS-arm64, windows-x64]
name: Make ${{ matrix.os }} release name: Make ${{ matrix.name }} release
needs: [build, jlink] needs: [build, jlink]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download Jar - name: Download Jar
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: jar name: jar
path: server/build path: server/build
- name: Download JRE - name: Download JRE
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all' if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
with: with:
name: ${{ matrix.os }}-jre name: ${{ matrix.jre }}-jre
path: jre path: jre
- name: Download icons - name: Download icons
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: icon name: icon
path: server/src/main/resources/icon path: server/src/main/resources/icon
- name: Download scripts.tar.gz - name: Download scripts.tar.gz
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: scripts name: scripts
- name: Make ${{ matrix.os }} release - name: Make ${{ matrix.name }} release
run: | run: |
mkdir upload mkdir upload
tar -xvpf scripts.tar.gz tar -xvpf scripts.tar.gz
scripts/bundler.sh -o upload/ ${{ matrix.os }} scripts/bundler.sh -o upload/ ${{ matrix.name }}
- name: Upload ${{ matrix.os }} release - name: Upload ${{ matrix.name }} release
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.os }} name: ${{ matrix.name }}
path: upload/* path: upload/*
if-no-files-found: error if-no-files-found: error
@@ -165,37 +174,41 @@ jobs:
needs: bundle needs: bundle
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: jar name: jar
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: debian-all name: debian-all
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with:
name: appimage
path: release
- uses: actions/download-artifact@v8
with: with:
name: linux-assets name: linux-assets
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: linux-x64 name: linux-x64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: macOS-x64 name: macOS-x64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: macOS-arm64 name: macOS-arm64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: windows-x64 name: windows-x64
path: release path: release
- name: Checkout Preview branch - name: Checkout Preview branch
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
repository: "Suwayomi/Suwayomi-Server-preview" repository: "Suwayomi/Suwayomi-Server-preview"
ref: main ref: main
@@ -227,7 +240,7 @@ jobs:
git push origin $TAG git push origin $TAG
- name: Upload Preview Release - name: Upload Preview Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }} token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
repository: "Suwayomi/Suwayomi-Server-preview" repository: "Suwayomi/Suwayomi-Server-preview"
+58 -45
View File
@@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3 uses: gradle/actions/wrapper-validation@v5
build: build:
name: Build Jar name: Build Jar
@@ -27,20 +27,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout ${{ github.ref }} - name: Checkout ${{ github.ref }}
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
path: master path: master
fetch-depth: 0 fetch-depth: 0
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 25
distribution: 'temurin' distribution: 'zulu'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
@@ -56,14 +56,14 @@ jobs:
run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace run: ./gradlew :server:downloadWebUI :server:shadowJar --stacktrace
- name: Upload Jar - name: Upload Jar
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: jar name: jar
path: master/server/build/*.jar path: master/server/build/*.jar
if-no-files-found: error if-no-files-found: error
- name: Upload icons - name: Upload icons
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: icon name: icon
path: master/server/src/main/resources/icon path: master/server/src/main/resources/icon
@@ -73,7 +73,7 @@ jobs:
run: tar -cvzf scripts.tar.gz -C master/ scripts/ run: tar -cvzf scripts.tar.gz -C master/ scripts/
- name: Upload scripts.tar.gz - name: Upload scripts.tar.gz
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: scripts name: scripts
path: scripts.tar.gz path: scripts.tar.gz
@@ -88,24 +88,24 @@ jobs:
name: linux-x64 name: linux-x64
- os: windows-latest - os: windows-latest
name: windows-x64 name: windows-x64
- os: macos-14 - os: macos-15
name: macOS-arm64 name: macOS-arm64
- os: macos-13 - os: macos-15-intel
name: macOS-x64 name: macOS-x64
os: [ubuntu-latest, windows-latest, macos-14, macos-13] os: [ubuntu-latest, windows-latest, macos-15, macos-15-intel]
steps: steps:
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 25
distribution: 'temurin' distribution: 'zulu'
- name: Package JDK - name: Package JDK
run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.random,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs --output suwa --strip-debug --no-man-pages --no-header-files --compress=2 run: jlink --add-modules java.base,java.compiler,java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,java.prefs,java.scripting,java.se,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,jdk.attach,jdk.crypto.ec,jdk.jdi,jdk.management,jdk.net,jdk.unsupported,jdk.unsupported.desktop,jdk.zipfs,jdk.accessibility --output suwa --strip-debug --no-man-pages --no-header-files --compress=2
- name: Upload JDK package - name: Upload JDK package
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.name }}-jre name: ${{ matrix.name }}-jre
path: suwa path: suwa
@@ -114,52 +114,61 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: include:
- debian-all - name: debian-all
- linux-assets jre: linux-x64
- linux-x64 - name: appimage
- macOS-x64 jre: linux-x64
- macOS-arm64 - name: linux-assets
- windows-x64 jre: linux-assets
- name: linux-x64
jre: linux-x64
- name: macOS-x64
jre: macOS-x64
- name: macOS-arm64
jre: macOS-arm64
- name: windows-x64
jre: windows-x64
name: [debian-all, appimage, linux-assets, linux-x64, macOS-x64, macOS-arm64, windows-x64]
name: Make ${{ matrix.os }} release name: Make ${{ matrix.name }} release
needs: [build, jlink] needs: [build, jlink]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download Jar - name: Download Jar
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: jar name: jar
path: server/build path: server/build
- name: Download JRE - name: Download JRE
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
if: matrix.os != 'linux-assets' && matrix.os != 'debian-all' if: matrix.name != 'linux-assets' && matrix.name != 'debian-all'
with: with:
name: ${{ matrix.os }}-jre name: ${{ matrix.jre }}-jre
path: jre path: jre
- name: Download icons - name: Download icons
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: icon name: icon
path: server/src/main/resources/icon path: server/src/main/resources/icon
- name: Download scripts.tar.gz - name: Download scripts.tar.gz
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: scripts name: scripts
- name: Make ${{ matrix.os }} release - name: Make ${{ matrix.name }} release
run: | run: |
mkdir upload/ mkdir upload/
tar -xvpf scripts.tar.gz tar -xvpf scripts.tar.gz
scripts/bundler.sh -o upload/ ${{ matrix.os }} scripts/bundler.sh -o upload/ ${{ matrix.name }}
- name: Upload ${{ matrix.os }} files - name: Upload ${{ matrix.name }} files
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: ${{ matrix.os }} name: ${{ matrix.name }}
path: upload/* path: upload/*
if-no-files-found: error if-no-files-found: error
@@ -168,31 +177,35 @@ jobs:
needs: bundle needs: bundle
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: jar name: jar
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: debian-all name: debian-all
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with:
name: appimage
path: release
- uses: actions/download-artifact@v8
with: with:
name: linux-assets name: linux-assets
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: linux-x64 name: linux-x64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: macOS-x64 name: macOS-x64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: macOS-arm64 name: macOS-arm64
path: release path: release
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v8
with: with:
name: windows-x64 name: windows-x64
path: release path: release
@@ -201,7 +214,7 @@ jobs:
run: cd release && sha256sum * > Checksums.sha256 run: cd release && sha256sum * > Checksums.sha256
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
token: ${{ secrets.DEPLOY_RELEASE_TOKEN }} token: ${{ secrets.DEPLOY_RELEASE_TOKEN }}
draft: true draft: true
+41
View File
@@ -0,0 +1,41 @@
name: GitHub Wiki upload
on:
push:
branches:
- master
paths: [docs/**, .github/workflows/wiki.yml]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
wiki:
name: Publish to GitHub Wiki
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
repository: ${{github.repository}}
path: ${{github.repository}}
- name: Checkout Wiki
uses: actions/checkout@v6
with:
repository: ${{github.repository}}.wiki
path: ${{github.repository}}.wiki
- name: Push to wiki
run: |
set -e
cd $GITHUB_WORKSPACE/${{github.repository}}.wiki
cp -r $GITHUB_WORKSPACE/${{github.repository}}/docs/* .
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
git diff-index --quiet HEAD || git commit -m "action: wiki sync" && git push
+9 -5
View File
@@ -1,15 +1,19 @@
name: Publish to WinGet name: Publish to WinGet
on: on:
workflow_run: workflow_dispatch:
workflows: ["CI Publish"] inputs:
types: version:
- completed description: Version
required: false
jobs: jobs:
publish: publish:
runs-on: windows-latest # action can only be run on windows runs-on: windows-latest # action can only be run on windows
steps: steps:
- uses: vedantmgoyal2009/winget-releaser@v2 - uses: vedantmgoyal2009/winget-releaser@v2
with: with:
identifier: Suwayomi.Tachidesk-Server identifier: Suwayomi.Suwayomi-Server
installers-regex: '.*x64.msi$' installers-regex: '.*x64.msi$'
token: ${{ secrets.WINGET_PUBLISH_PAT }} token: ${{ secrets.WINGET_PUBLISH_PAT }}
version: ${{ inputs.version || github.ref_name }}
release-tag: ${{ inputs.version || github.ref_name }}
+1
View File
@@ -20,3 +20,4 @@ scripts/OpenJDK*
scripts/zulu* scripts/zulu*
scripts/electron-* scripts/electron-*
scripts/rcedit-* scripts/rcedit-*
scripts/resources/*.so
+1734
View File
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ package xyz.nulldev.ts.config
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import net.harawata.appdirs.AppDirsFactory import ca.gosyer.appdirs.AppDirs
const val CONFIG_PREFIX = "suwayomi.tachidesk.config" const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
@@ -15,6 +15,6 @@ val ApplicationRootDir: String
get(): String { get(): String {
return System.getProperty( return System.getProperty(
"$CONFIG_PREFIX.server.rootDir", "$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null), AppDirs { appName = "Tachidesk" }.getUserDataDir(),
) )
} }
@@ -10,10 +10,11 @@ package xyz.nulldev.ts.config
import ch.qos.logback.classic.Level import ch.qos.logback.classic.Level
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigObject
import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.parser.ConfigDocument import com.typesafe.config.parser.ConfigDocument
import com.typesafe.config.parser.ConfigDocumentFactory import com.typesafe.config.parser.ConfigDocumentFactory
import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -112,56 +113,87 @@ open class ConfigManager {
value: Any, value: Any,
) { ) {
mutex.withLock { mutex.withLock {
val configValue = ConfigValueFactory.fromAnyRef(value) val configValue = value.toConfig("internal").getValue("internal")
updateUserConfigFile(path, configValue) updateUserConfigFile(path, configValue)
internalConfig = internalConfig.withValue(path, configValue) internalConfig = internalConfig.withValue(path, configValue)
} }
} }
fun resetUserConfig(updateInternalConfig: Boolean = true): ConfigDocument { private fun createConfigDocumentFromReference(): ConfigDocument {
val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText() val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText()
val serverConfigDoc = ConfigDocumentFactory.parseString(serverConfigFileContent) return ConfigDocumentFactory.parseString(serverConfigFileContent)
userConfigFile.writeText(serverConfigDoc.render())
if (updateInternalConfig) {
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
} }
fun resetUserConfig(): ConfigDocument {
val serverConfigDoc = createConfigDocumentFromReference()
userConfigFile.writeText(serverConfigDoc.render())
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
return serverConfigDoc return serverConfigDoc
} }
/** /**
* Makes sure the "UserConfig" is up-to-date. * Makes sure the "UserConfig" is up-to-date.
* *
* - adds missing settings * - Adds missing settings
* - removes outdated settings * - Migrates deprecated settings
* - Removes outdated settings
*/ */
fun updateUserConfig() { fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) {
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val userConfig = getUserConfig() val userConfig = getUserConfig()
val hasMissingSettings = serverConfig.entrySet().any { !userConfig.hasPath(it.key) } // NOTE: if more than 1 dot is included, that's a nested setting, which we need to filter out here
val hasOutdatedSettings = userConfig.entrySet().any { !serverConfig.hasPath(it.key) } val refKeys =
serverConfig.root().entries.flatMap {
(it.value as? ConfigObject)?.entries?.map { e -> "${it.key}.${e.key}" }.orEmpty()
}
val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) }
val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 }
val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings
if (!isUserConfigOutdated) { if (!isUserConfigOutdated) {
return return
} }
logger.debug { logger.debug {
"user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings" "user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings)"
} }
var newUserConfigDoc: ConfigDocument = resetUserConfig(false) var newUserConfigDoc: ConfigDocument = createConfigDocumentFromReference()
userConfig userConfig
.entrySet() .entrySet()
.filter { .filter {
serverConfig.hasPath( serverConfig.hasPath(
it.key, it.key,
) ) ||
it.key.count { c -> c == '.' } > 1
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) } }.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
newUserConfigDoc =
migrate(newUserConfigDoc, internalConfig)
userConfigFile.writeText(newUserConfigDoc.render()) userConfigFile.writeText(newUserConfigDoc.render())
getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) }
}
fun getRedactedConfig(nonPrivacySafeKeys: List<String>): Config {
val entries =
config.entrySet().associate { entry ->
val key = entry.key
val value =
if (nonPrivacySafeKeys.any { key.split(".").getOrNull(1) == it }) {
"[REDACTED]"
} else {
entry.value.unwrapped()
}
key to value
}
return ConfigFactory.parseMap(entries)
} }
} }
@@ -8,9 +8,12 @@ package xyz.nulldev.ts.config
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValueFactory import io.github.config4k.ClassContainer
import io.github.config4k.TypeReference
import io.github.config4k.getValue import io.github.config4k.getValue
import io.github.config4k.readers.SelectReader
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
/** /**
@@ -26,7 +29,7 @@ abstract class ConfigModule(
*/ */
abstract class SystemPropertyOverridableConfigModule( abstract class SystemPropertyOverridableConfigModule(
getConfig: () -> Config, getConfig: () -> Config,
moduleName: String, val moduleName: String,
) : ConfigModule(getConfig) { ) : ConfigModule(getConfig) {
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName) val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
} }
@@ -41,25 +44,33 @@ class SystemPropertyOverrideDelegate(
property: KProperty<*>, property: KProperty<*>,
): T { ): T {
val config = getConfig() val config = getConfig()
val systemProperty =
System.getProperty("$CONFIG_PREFIX.$moduleName.${property.name}")
if (systemProperty == null) {
val configValue: T = config.getValue(thisRef, property) val configValue: T = config.getValue(thisRef, property)
return configValue
}
val combined = val systemPropertyConfig =
System.getProperty( try {
"$CONFIG_PREFIX.$moduleName.${property.name}", ConfigFactory.parseString("internal=$systemProperty")
if (T::class.simpleName == "List") { } catch (_: ConfigException) {
ConfigValueFactory.fromAnyRef(configValue).render() ConfigFactory.parseMap(mapOf("internal" to systemProperty))
} else { }
configValue.toString()
},
)
return when (T::class.simpleName) { val genericType = object : TypeReference<T>() {}.genericType()
"Int" -> combined.toInt() val clazz = ClassContainer(T::class, genericType)
"Boolean" -> combined.toBoolean() val reader = SelectReader.getReader(clazz)
"Double" -> combined.toDouble() val path = property.name
"List" -> ConfigFactory.parseString("internal=" + combined).getStringList("internal").orEmpty()
// add more types as needed val result = reader(systemPropertyConfig, "internal")
else -> combined // covers String return try {
} as T result as T
} catch (e: Exception) {
throw result
?.let { e }
?: ConfigException.BadPath(path, "take a look at your config")
}
} }
} }
+1
View File
@@ -47,4 +47,5 @@ dependencies {
// OpenJDK lacks native JPEG encoder and native WEBP decoder // OpenJDK lacks native JPEG encoder and native WEBP decoder
implementation(libs.bundles.twelvemonkeys) implementation(libs.bundles.twelvemonkeys)
implementation(libs.imageio.webp)
} }
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2016 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.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
/**
* <p>Denotes that the annotated element represents a packed color
* long. If applied to a long array, every element in the array
* represents a color long. For more information on how colors
* are packed in a long, please refer to the documentation of
* the {@link android.graphics.Color} class.</p>
*
* <p>Example:</p>
*
* <pre>{@code
* public void setFillColor(@ColorLong long color);
* }</pre>
*
* @see android.graphics.Color
*
* @hide
*/
@Retention(SOURCE)
@Target({PARAMETER,METHOD,LOCAL_VARIABLE,FIELD})
public @interface ColorLong {
}
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2016 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.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
/**
* <p>Denotes that the annotated element represents a half-precision floating point
* value. Such values are stored in short data types and can be manipulated with
* the {@link android.util.Half} class. If applied to an array of short, every
* element in the array represents a half-precision float.</p>
*
* <p>Example:</p>
*
* <pre>{@code
* public abstract void setPosition(@HalfFloat short x, @HalfFloat short y, @HalfFloat short z);
* }</pre>
*
* @see android.util.Half
* @see android.util.Half#toHalf(float)
* @see android.util.Half#toFloat(short)
*
* @hide
*/
@Retention(SOURCE)
@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
public @interface HalfFloat {
}
@@ -2,17 +2,19 @@ package android.graphics;
import android.annotation.ColorInt; import android.annotation.ColorInt;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Log;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter; import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
public final class Bitmap { public final class Bitmap {
private final int width; private final int width;
@@ -58,7 +60,23 @@ public final class Bitmap {
ARGB_8888(5), ARGB_8888(5),
RGBA_F16(6), RGBA_F16(6),
HARDWARE(7), HARDWARE(7),
RGBA_1010102(8); RGBA_1010102(8),
_TYPE_3BYTE_BGR(BufferedImage.TYPE_3BYTE_BGR),
_TYPE_4BYTE_ABGR(BufferedImage.TYPE_4BYTE_ABGR),
_TYPE_4BYTE_ABGR_PRE(BufferedImage.TYPE_4BYTE_ABGR_PRE),
_TYPE_BYTE_BINARY(BufferedImage.TYPE_BYTE_BINARY),
_TYPE_BYTE_GRAY(BufferedImage.TYPE_BYTE_GRAY),
_TYPE_BYTE_INDEXED(BufferedImage.TYPE_BYTE_INDEXED),
_TYPE_CUSTOM(BufferedImage.TYPE_CUSTOM),
_TYPE_INT_ARGB(BufferedImage.TYPE_INT_ARGB),
_TYPE_INT_ARGB_PRE(BufferedImage.TYPE_INT_ARGB_PRE),
_TYPE_INT_BGR(BufferedImage.TYPE_INT_BGR),
_TYPE_INT_RGB(BufferedImage.TYPE_INT_RGB),
_TYPE_USHORT_555_RGB(BufferedImage.TYPE_USHORT_555_RGB),
_TYPE_USHORT_565_RGB(BufferedImage.TYPE_USHORT_565_RGB),
_TYPE_USHORT_GRAY(BufferedImage.TYPE_USHORT_GRAY),
;
final int nativeInt; final int nativeInt;
@@ -75,6 +93,70 @@ public final class Bitmap {
} }
} }
private static int configToBufferedImageType(Config config) {
switch (config) {
case ALPHA_8:
return BufferedImage.TYPE_BYTE_GRAY;
case RGB_565:
return BufferedImage.TYPE_USHORT_565_RGB;
case ARGB_8888:
return BufferedImage.TYPE_INT_ARGB;
case _TYPE_3BYTE_BGR:
case _TYPE_4BYTE_ABGR:
case _TYPE_4BYTE_ABGR_PRE:
case _TYPE_BYTE_BINARY:
case _TYPE_BYTE_GRAY:
case _TYPE_BYTE_INDEXED:
case _TYPE_CUSTOM:
case _TYPE_INT_ARGB:
case _TYPE_INT_ARGB_PRE:
case _TYPE_INT_BGR:
case _TYPE_INT_RGB:
case _TYPE_USHORT_555_RGB:
case _TYPE_USHORT_565_RGB:
case _TYPE_USHORT_GRAY:
return config.ordinal();
default:
throw new UnsupportedOperationException("Bitmap.Config(" + config + ") not supported");
}
}
private static Config bufferedImageTypeToConfig(int type) {
switch (type) {
case BufferedImage.TYPE_BYTE_GRAY:
return Config.ALPHA_8;
case BufferedImage.TYPE_USHORT_565_RGB:
return Config.RGB_565;
case BufferedImage.TYPE_INT_ARGB:
return Config.ARGB_8888;
case BufferedImage.TYPE_3BYTE_BGR:
return Config._TYPE_3BYTE_BGR;
case BufferedImage.TYPE_4BYTE_ABGR:
return Config._TYPE_4BYTE_ABGR;
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
return Config._TYPE_4BYTE_ABGR_PRE;
case BufferedImage.TYPE_BYTE_BINARY:
return Config._TYPE_BYTE_BINARY;
case BufferedImage.TYPE_BYTE_INDEXED:
return Config._TYPE_BYTE_INDEXED;
case BufferedImage.TYPE_CUSTOM:
return Config._TYPE_CUSTOM;
case BufferedImage.TYPE_INT_ARGB_PRE:
return Config._TYPE_INT_ARGB_PRE;
case BufferedImage.TYPE_INT_BGR:
return Config._TYPE_INT_BGR;
case BufferedImage.TYPE_INT_RGB:
return Config._TYPE_INT_RGB;
case BufferedImage.TYPE_USHORT_555_RGB:
return Config._TYPE_USHORT_555_RGB;
case BufferedImage.TYPE_USHORT_GRAY:
return Config._TYPE_USHORT_GRAY;
default:
Log.w("Bitmap", "Encountered unsupported image type " + type);
return null;
}
}
/** /**
* Common code for checking that x and y are >= 0 * Common code for checking that x and y are >= 0
* *
@@ -106,7 +188,7 @@ public final class Bitmap {
} }
public static Bitmap createBitmap(int width, int height, Config config) { public static Bitmap createBitmap(int width, int height, Config config) {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); BufferedImage image = new BufferedImage(width, height, configToBufferedImageType(config));
return new Bitmap(image); return new Bitmap(image);
} }
@@ -144,8 +226,10 @@ public final class Bitmap {
formatString = "png"; formatString = "png";
} else if (format == Bitmap.CompressFormat.JPEG) { } else if (format == Bitmap.CompressFormat.JPEG) {
formatString = "jpg"; formatString = "jpg";
} else if (format == Bitmap.CompressFormat.WEBP || format == Bitmap.CompressFormat.WEBP_LOSSY) {
formatString = "webp";
} else { } else {
throw new IllegalArgumentException("unsupported compression format!"); throw new IllegalArgumentException("unsupported compression format! " + format);
} }
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(formatString); Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(formatString);
@@ -162,14 +246,19 @@ public final class Bitmap {
} }
writer.setOutput(ios); writer.setOutput(ios);
BufferedImage img = image;
ImageWriteParam param = writer.getDefaultWriteParam(); ImageWriteParam param = writer.getDefaultWriteParam();
if ("jpg".equals(formatString)) { if ("jpg".equals(formatString)) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(qualityFloat); param.setCompressionQuality(qualityFloat);
img = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
img.getGraphics().drawImage(image, 0, 0, null);
} }
try { try {
writer.write(null, new IIOImage(image, null, null), param); writer.write(null, new IIOImage(img, null, null), param);
ios.close(); ios.close();
writer.dispose(); writer.dispose();
} catch (IOException ex) { } catch (IOException ex) {
@@ -179,6 +268,12 @@ public final class Bitmap {
return true; return true;
} }
public Bitmap copy(Config config, boolean isMutable) {
Bitmap ret = createBitmap(width, height, config);
ret.image.getGraphics().drawImage(image, 0, 0, null);
return ret;
}
/** /**
* Shared code to check for illegal arguments passed to getPixels() * Shared code to check for illegal arguments passed to getPixels()
* or setPixels() * or setPixels()
@@ -220,16 +315,102 @@ public final class Bitmap {
} }
} }
/**
* Shared code to check for illegal arguments passed to getPixel()
* or setPixel()
*
* @param x x coordinate of the pixel
* @param y y coordinate of the pixel
*/
private void checkPixelAccess(int x, int y) {
checkXYSign(x, y);
if (x >= getWidth()) {
throw new IllegalArgumentException("x must be < bitmap.width()");
}
if (y >= getHeight()) {
throw new IllegalArgumentException("y must be < bitmap.height()");
}
}
public void getPixels(@ColorInt int[] pixels, int offset, int stride, public void getPixels(@ColorInt int[] pixels, int offset, int stride,
int x, int y, int width, int height) { int x, int y, int width, int height) {
checkPixelsAccess(x, y, width, height, offset, stride, pixels); checkPixelsAccess(x, y, width, height, offset, stride, pixels);
Raster raster = image.getData(); image.getRGB(x, y, width, height, pixels, offset, stride);
int[] rasterPixels = raster.getPixels(x, y, width, height, (int[]) null); }
for (int ht = 0; ht < height; ht++) { @ColorInt
int rowOffset = offset + stride * ht; public int getPixel(int x, int y) {
System.arraycopy(rasterPixels, ht * width, pixels, rowOffset, width); checkPixelAccess(x, y);
} return image.getRGB(x, y);
}
/**
* <p>Write the specified {@link Color} into the bitmap (assuming it is
* mutable) at the x,y coordinate. The color must be a
* non-premultiplied ARGB value in the {@link ColorSpace.Named#SRGB sRGB}
* color space.</p>
*
* @param x The x coordinate of the pixel to replace (0...width-1)
* @param y The y coordinate of the pixel to replace (0...height-1)
* @param color The ARGB color to write into the bitmap
*
* @throws IllegalStateException if the bitmap is not mutable
* @throws IllegalArgumentException if x, y are outside of the bitmap's
* bounds.
*/
public void setPixel(int x, int y, @ColorInt int color) {
checkPixelAccess(x, y);
image.setRGB(x, y, color);
}
/**
* <p>Replace pixels in the bitmap with the colors in the array. Each element
* in the array is a packed int representing a non-premultiplied ARGB
* {@link Color} in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
*
* @param pixels The colors to write to the bitmap
* @param offset The index of the first color to read from pixels[]
* @param stride The number of colors in pixels[] to skip between rows.
* Normally this value will be the same as the width of
* the bitmap, but it can be larger (or negative).
* @param x The x coordinate of the first pixel to write to in
* the bitmap.
* @param y The y coordinate of the first pixel to write to in
* the bitmap.
* @param width The number of colors to copy from pixels[] per row
* @param height The number of rows to write to the bitmap
*
* @throws IllegalStateException if the bitmap is not mutable
* @throws IllegalArgumentException if x, y, width, height are outside of
* the bitmap's bounds.
* @throws ArrayIndexOutOfBoundsException if the pixels array is too small
* to receive the specified number of pixels.
*/
public void setPixels(@NonNull @ColorInt int[] pixels, int offset, int stride,
int x, int y, int width, int height) {
if (width == 0 || height == 0) {
return; // nothing to do
}
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
image.setRGB(x, y, width, height, pixels, offset, stride);
}
public void eraseColor(int c) {
java.awt.Color color = Color.valueOf(c).toJavaColor();
Graphics2D graphics = image.createGraphics();
graphics.setColor(color);
graphics.fillRect(0, 0, width, height);
graphics.dispose();
}
public void recycle() {
// do nothing
}
@Nullable
public final Config getConfig() {
int type = image.getType();
return bufferedImageTypeToConfig(type);
} }
} }
@@ -8,13 +8,462 @@ import java.util.Iterator;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageReader; import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import androidx.annotation.Nullable;
public class BitmapFactory { public class BitmapFactory {
public static class Options {
/**
* Create a default Options object, which if left unchanged will give
* the same result from the decoder as if null were passed.
*/
public Options() {
inScaled = true;
inPremultiplied = true;
}
/**
* If set, decode methods that take the Options object will attempt to
* reuse this bitmap when loading content. If the decode operation
* cannot use this bitmap, the decode method will throw an
* {@link java.lang.IllegalArgumentException}. The
* current implementation necessitates that the reused bitmap be
* mutable, and the resulting reused bitmap will continue to remain
* mutable even when decoding a resource which would normally result in
* an immutable bitmap.</p>
*
* <p>You should still always use the returned Bitmap of the decode
* method and not assume that reusing the bitmap worked, due to the
* constraints outlined above and failure situations that can occur.
* Checking whether the return value matches the value of the inBitmap
* set in the Options structure will indicate if the bitmap was reused,
* but in all cases you should use the Bitmap returned by the decoding
* function to ensure that you are using the bitmap that was used as the
* decode destination.</p>
*
* <h3>Usage with BitmapFactory</h3>
*
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, any
* mutable bitmap can be reused by {@link BitmapFactory} to decode any
* other bitmaps as long as the resulting {@link Bitmap#getByteCount()
* byte count} of the decoded bitmap is less than or equal to the {@link
* Bitmap#getAllocationByteCount() allocated byte count} of the reused
* bitmap. This can be because the intrinsic size is smaller, or its
* size post scaling (for density / sample size) is smaller.</p>
*
* <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT}
* additional constraints apply: The image being decoded (whether as a
* resource or as a stream) must be in jpeg or png format. Only equal
* sized bitmaps are supported, with {@link #inSampleSize} set to 1.
* Additionally, the {@link android.graphics.Bitmap.Config
* configuration} of the reused bitmap will override the setting of
* {@link #inPreferredConfig}, if set.</p>
*
* <h3>Usage with BitmapRegionDecoder</h3>
*
* <p>BitmapRegionDecoder will draw its requested content into the Bitmap
* provided, clipping if the output content size (post scaling) is larger
* than the provided Bitmap. The provided Bitmap's width, height, and
* {@link Bitmap.Config} will not be changed.
*
* <p class="note">BitmapRegionDecoder support for {@link #inBitmap} was
* introduced in {@link android.os.Build.VERSION_CODES#JELLY_BEAN}. All
* formats supported by BitmapRegionDecoder support Bitmap reuse via
* {@link #inBitmap}.</p>
*
* @see Bitmap#reconfigure(int,int, android.graphics.Bitmap.Config)
*/
public Bitmap inBitmap;
/**
* If set, decode methods will always return a mutable Bitmap instead of
* an immutable one. This can be used for instance to programmatically apply
* effects to a Bitmap loaded through BitmapFactory.
* <p>Can not be set simultaneously with inPreferredConfig =
* {@link android.graphics.Bitmap.Config#HARDWARE},
* because hardware bitmaps are always immutable.
*/
public boolean inMutable;
/**
* If set to true, the decoder will return null (no bitmap), but
* the <code>out...</code> fields will still be set, allowing the caller to
* query the bitmap without having to allocate the memory for its pixels.
*/
public boolean inJustDecodeBounds;
/**
* If set to a value > 1, requests the decoder to subsample the original
* image, returning a smaller image to save memory. The sample size is
* the number of pixels in either dimension that correspond to a single
* pixel in the decoded bitmap. For example, inSampleSize == 4 returns
* an image that is 1/4 the width/height of the original, and 1/16 the
* number of pixels. Any value <= 1 is treated the same as 1. Note: the
* decoder uses a final value based on powers of 2, any other value will
* be rounded down to the nearest power of 2.
*/
public int inSampleSize;
/**
* If this is non-null, the decoder will try to decode into this
* internal configuration. If it is null, or the request cannot be met,
* the decoder will try to pick the best matching config based on the
* system's screen depth, and characteristics of the original image such
* as if it has per-pixel alpha (requiring a config that also does).
*
* Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by
* default.
*/
public Bitmap.Config inPreferredConfig = null;
/**
* <p>If this is non-null, the decoder will try to decode into this
* color space. If it is null, or the request cannot be met,
* the decoder will pick either the color space embedded in the image
* or the color space best suited for the requested image configuration
* (for instance {@link ColorSpace.Named#SRGB sRGB} for
* {@link Bitmap.Config#ARGB_8888} configuration and
* {@link ColorSpace.Named#EXTENDED_SRGB EXTENDED_SRGB} for
* {@link Bitmap.Config#RGBA_F16}).</p>
*
* <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
* currently supported. An <code>IllegalArgumentException</code> will
* be thrown by the decode methods when setting a non-RGB color space
* such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
*
* <p class="note">
* Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
* the specified color space's transfer function must be
* an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
* <code>IllegalArgumentException</code> will be thrown by the decode methods
* if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
* specified color space returns null.
*
* Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
* non ICC parametric curve transfer function is allowed.
* E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.</p>
*
* <p>After decode, the bitmap's color space is stored in
* {@link #outColorSpace}.</p>
*/
public ColorSpace inPreferredColorSpace = null;
/**
* If true (which is the default), the resulting bitmap will have its
* color channels pre-multiplied by the alpha channel.
*
* <p>This should NOT be set to false for images to be directly drawn by
* the view system or through a {@link Canvas}. The view system and
* {@link Canvas} assume all drawn images are pre-multiplied to simplify
* draw-time blending, and will throw a RuntimeException when
* un-premultiplied are drawn.</p>
*
* <p>This is likely only useful if you want to manipulate raw encoded
* image data, e.g. with RenderScript or custom OpenGL.</p>
*
* <p>This does not affect bitmaps without an alpha channel.</p>
*
* <p>Setting this flag to false while setting {@link #inScaled} to true
* may result in incorrect colors.</p>
*
* @see Bitmap#hasAlpha()
* @see Bitmap#isPremultiplied()
* @see #inScaled
*/
public boolean inPremultiplied;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
* ignored.
*
* In {@link android.os.Build.VERSION_CODES#M} and below, if dither is
* true, the decoder will attempt to dither the decoded image.
*/
@Deprecated
public boolean inDither;
/**
* The pixel density to use for the bitmap. This will always result
* in the returned bitmap having a density set for it (see
* {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
* if {@link #inScaled} is set (which it is by default} and this
* density does not match {@link #inTargetDensity}, then the bitmap
* will be scaled to the target density before being returned.
*
* <p>If this is 0,
* {@link BitmapFactory#decodeResource(Resources, int)},
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
* and {@link BitmapFactory#decodeResourceStream}
* will fill in the density associated with the resource. The other
* functions will leave it as-is and no density will be applied.
*
* @see #inTargetDensity
* @see #inScreenDensity
* @see #inScaled
* @see Bitmap#setDensity(int)
* @see android.util.DisplayMetrics#densityDpi
*/
public int inDensity;
/**
* The pixel density of the destination this bitmap will be drawn to.
* This is used in conjunction with {@link #inDensity} and
* {@link #inScaled} to determine if and how to scale the bitmap before
* returning it.
*
* <p>If this is 0,
* {@link BitmapFactory#decodeResource(Resources, int)},
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
* and {@link BitmapFactory#decodeResourceStream}
* will fill in the density associated the Resources object's
* DisplayMetrics. The other
* functions will leave it as-is and no scaling for density will be
* performed.
*
* @see #inDensity
* @see #inScreenDensity
* @see #inScaled
* @see android.util.DisplayMetrics#densityDpi
*/
public int inTargetDensity;
/**
* The pixel density of the actual screen that is being used. This is
* purely for applications running in density compatibility code, where
* {@link #inTargetDensity} is actually the density the application
* sees rather than the real screen density.
*
* <p>By setting this, you
* allow the loading code to avoid scaling a bitmap that is currently
* in the screen density up/down to the compatibility density. Instead,
* if {@link #inDensity} is the same as {@link #inScreenDensity}, the
* bitmap will be left as-is. Anything using the resulting bitmap
* must also used {@link Bitmap#getScaledWidth(int)
* Bitmap.getScaledWidth} and {@link Bitmap#getScaledHeight
* Bitmap.getScaledHeight} to account for any different between the
* bitmap's density and the target's density.
*
* <p>This is never set automatically for the caller by
* {@link BitmapFactory} itself. It must be explicitly set, since the
* caller must deal with the resulting bitmap in a density-aware way.
*
* @see #inDensity
* @see #inTargetDensity
* @see #inScaled
* @see android.util.DisplayMetrics#densityDpi
*/
public int inScreenDensity;
/**
* When this flag is set, if {@link #inDensity} and
* {@link #inTargetDensity} are not 0, the
* bitmap will be scaled to match {@link #inTargetDensity} when loaded,
* rather than relying on the graphics system scaling it each time it
* is drawn to a Canvas.
*
* <p>BitmapRegionDecoder ignores this flag, and will not scale output
* based on density. (though {@link #inSampleSize} is supported)</p>
*
* <p>This flag is turned on by default and should be turned off if you need
* a non-scaled version of the bitmap. Nine-patch bitmaps ignore this
* flag and are always scaled.
*
* <p>If {@link #inPremultiplied} is set to false, and the image has alpha,
* setting this flag to true may result in incorrect colors.
*/
public boolean inScaled;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
* ignored.
*
* In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
* is set to true, then the resulting bitmap will allocate its
* pixels such that they can be purged if the system needs to reclaim
* memory. In that instance, when the pixels need to be accessed again
* (e.g. the bitmap is drawn, getPixels() is called), they will be
* automatically re-decoded.
*
* <p>For the re-decode to happen, the bitmap must have access to the
* encoded data, either by sharing a reference to the input
* or by making a copy of it. This distinction is controlled by
* inInputShareable. If this is true, then the bitmap may keep a shallow
* reference to the input. If this is false, then the bitmap will
* explicitly make a copy of the input data, and keep that. Even if
* sharing is allowed, the implementation may still decide to make a
* deep copy of the input data.</p>
*
* <p>While inPurgeable can help avoid big Dalvik heap allocations (from
* API level 11 onward), it sacrifices performance predictability since any
* image that the view system tries to draw may incur a decode delay which
* can lead to dropped frames. Therefore, most apps should avoid using
* inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
* allocations use the {@link #inBitmap} flag instead.</p>
*
* <p class="note"><strong>Note:</strong> This flag is ignored when used
* with {@link #decodeResource(Resources, int,
* android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
* android.graphics.BitmapFactory.Options)}.</p>
*/
@Deprecated
public boolean inPurgeable;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
* ignored.
*
* In {@link android.os.Build.VERSION_CODES#KITKAT} and below, this
* field works in conjunction with inPurgeable. If inPurgeable is false,
* then this field is ignored. If inPurgeable is true, then this field
* determines whether the bitmap can share a reference to the input
* data (inputstream, array, etc.) or if it must make a deep copy.
*/
@Deprecated
public boolean inInputShareable;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is
* ignored. The output will always be high quality.
*
* In {@link android.os.Build.VERSION_CODES#M} and below, if
* inPreferQualityOverSpeed is set to true, the decoder will try to
* decode the reconstructed image to a higher quality even at the
* expense of the decoding speed. Currently the field only affects JPEG
* decode, in the case of which a more accurate, but slightly slower,
* IDCT method will be used instead.
*/
@Deprecated
public boolean inPreferQualityOverSpeed;
/**
* The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
* set to false, this will be width of the output bitmap after any
* scaling is applied. If true, it will be the width of the input image
* without any accounting for scaling.
*
* <p>outWidth will be set to -1 if there is an error trying to decode.</p>
*/
public int outWidth;
/**
* The resulting height of the bitmap. If {@link #inJustDecodeBounds} is
* set to false, this will be height of the output bitmap after any
* scaling is applied. If true, it will be the height of the input image
* without any accounting for scaling.
*
* <p>outHeight will be set to -1 if there is an error trying to decode.</p>
*/
public int outHeight;
/**
* If known, this string is set to the mimetype of the decoded image.
* If not known, or there is an error, it is set to null.
*/
public String outMimeType;
/**
* If known, the config the decoded bitmap will have.
* If not known, or there is an error, it is set to null.
*/
public Bitmap.Config outConfig;
/**
* If known, the color space the decoded bitmap will have. Note that the
* output color space is not guaranteed to be the color space the bitmap
* is encoded with. If not known (when the config is
* {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
* it is set to null.
*/
public ColorSpace outColorSpace;
/**
* Temp storage to use for decoding. Suggest 16K or so.
*/
public byte[] inTempStorage;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, see
* comments on {@link #requestCancelDecode()}.
*
* Flag to indicate that cancel has been called on this object. This
* is useful if there's an intermediary that wants to first decode the
* bounds and then decode the image. In that case the intermediary
* can check, inbetween the bounds decode and the image decode, to see
* if the operation is canceled.
*/
@Deprecated
public boolean mCancel;
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this
* will not affect the decode, though it will still set mCancel.
*
* In {@link android.os.Build.VERSION_CODES#M} and below, if this can
* be called from another thread while this options object is inside
* a decode... call. Calling this will notify the decoder that it
* should cancel its operation. This is not guaranteed to cancel the
* decode, but if it does, the decoder... operation will return null,
* or if inJustDecodeBounds is true, will set outWidth/outHeight
* to -1
*/
@Deprecated
public void requestCancelDecode() {
mCancel = true;
}
static void validate(Options opts) {
if (opts == null) return;
if (opts.inBitmap != null) {
/*
if (opts.inBitmap.getConfig() == Bitmap.Config.HARDWARE) {
throw new IllegalArgumentException(
"Bitmaps with Config.HARDWARE are always immutable");
}
if (opts.inBitmap.isRecycled()) {
throw new IllegalArgumentException(
"Cannot reuse a recycled Bitmap");
}
*/
}
if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) {
throw new IllegalArgumentException("Bitmaps with Config.HARDWARE cannot be " +
"decoded into - they are immutable");
}
if (opts.inPreferredColorSpace != null) {
if (!(opts.inPreferredColorSpace instanceof ColorSpace.Rgb)) {
throw new IllegalArgumentException("The destination color space must use the " +
"RGB color model");
}
if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG))
&& !opts.inPreferredColorSpace.equals(
ColorSpace.get(ColorSpace.Named.BT2020_PQ))
&& ((ColorSpace.Rgb) opts.inPreferredColorSpace)
.getTransferParameters() == null) {
throw new IllegalArgumentException("The destination color space must use an " +
"ICC parametric transfer function");
}
}
}
}
public static Bitmap decodeStream(InputStream inputStream) { public static Bitmap decodeStream(InputStream inputStream) {
return decodeStream(inputStream, null, null);
}
@Nullable
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts) {
if (is == null) return null;
if (outPadding != null) throw new RuntimeException("OutPadding is not implemented");
Options.validate(opts);
Bitmap bitmap = null; Bitmap bitmap = null;
// TODO: Support options with in parameters
try { try {
ImageInputStream imageInputStream = ImageIO.createImageInputStream(inputStream); ImageInputStream imageInputStream = ImageIO.createImageInputStream(is);
Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream); Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
if (!imageReaders.hasNext()) { if (!imageReaders.hasNext()) {
@@ -24,8 +473,18 @@ public class BitmapFactory {
ImageReader imageReader = imageReaders.next(); ImageReader imageReader = imageReaders.next();
imageReader.setInput(imageInputStream); imageReader.setInput(imageInputStream);
if (opts != null) {
opts.outHeight = imageReader.getHeight(0);
opts.outWidth = imageReader.getWidth(0);
opts.outMimeType = imageReader.getOriginatingProvider().getMIMETypes()[0];
opts.outColorSpace = null; // TODO: support? see imageReader.getImageTypeSpecifier().getColorSpace()
opts.outConfig = null; // TODO: support?
}
if (opts == null || !opts.inJustDecodeBounds) {
BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam()); BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam());
bitmap = new Bitmap(image); bitmap = new Bitmap(image);
}
imageReader.dispose(); imageReader.dispose();
} catch (IOException ex) { } catch (IOException ex) {
@@ -36,16 +495,11 @@ public class BitmapFactory {
} }
public static Bitmap decodeByteArray(byte[] data, int offset, int length) { public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
Bitmap bitmap = null; return decodeByteArray(data, offset, length, null);
ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data);
try {
BufferedImage image = ImageIO.read(byteArrayStream);
bitmap = new Bitmap(image);
} catch (IOException ex) {
throw new RuntimeException(ex);
} }
return bitmap; public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data, offset, length);
return decodeStream(byteArrayStream, null, opts);
} }
} }
@@ -1,12 +1,30 @@
package android.graphics; package android.graphics;
import android.annotation.ColorInt;
import android.annotation.ColorLong;
import android.annotation.NonNull;
import android.graphics.Path;
import android.graphics.RectF;
import java.awt.BasicStroke;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.TextAttribute;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import javax.imageio.ImageIO; import java.text.AttributedString;
import java.util.ArrayList;
import java.util.List;
public final class Canvas { public final class Canvas {
private BufferedImage canvasImage; private BufferedImage canvasImage;
private Graphics2D canvas; private Graphics2D canvas;
private List<AffineTransform> transformStack = new ArrayList<AffineTransform>();
private static final String TAG = "Canvas";
public Canvas(Bitmap bitmap) { public Canvas(Bitmap bitmap) {
canvasImage = bitmap.getImage(); canvasImage = bitmap.getImage();
@@ -16,6 +34,210 @@ public final class Canvas {
public void drawBitmap(Bitmap sourceBitmap, Rect src, Rect dst, Paint paint) { public void drawBitmap(Bitmap sourceBitmap, Rect src, Rect dst, Paint paint) {
BufferedImage sourceImage = sourceBitmap.getImage(); BufferedImage sourceImage = sourceBitmap.getImage();
BufferedImage sourceImageCropped = sourceImage.getSubimage(src.left, src.top, src.getWidth(), src.getHeight()); BufferedImage sourceImageCropped = sourceImage.getSubimage(src.left, src.top, src.getWidth(), src.getHeight());
canvas.drawImage(sourceImageCropped, null, dst.left, dst.top); canvas.drawImage(sourceImageCropped, dst.left, dst.top, dst.getWidth(), dst.getHeight(), null);
}
public void drawBitmap(Bitmap sourceBitmap, float left, float top, Paint paint) {
BufferedImage sourceImage = sourceBitmap.getImage();
canvas.drawImage(sourceImage, null, (int) left, (int) top);
}
public void drawText(@NonNull char[] text, int index, int count, float x, float y,
@NonNull Paint paint) {
drawText(new String(text, index, count), x, y, paint);
}
public void drawText(@NonNull String str, float x, float y, @NonNull Paint paint) {
applyPaint(paint);
AttributedString text = paint.getTypeface().createWithFallback(str);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
// TODO: fix with fallback fonts
GlyphVector glyphVector = paint.getTypeface().getFont().createGlyphVector(canvas.getFontRenderContext(), text.getIterator());
Shape textShape = glyphVector.getOutline();
switch (paint.getStyle()) {
case Paint.Style.FILL:
canvas.drawString(text.getIterator(), x, y);
break;
case Paint.Style.STROKE:
save();
translate(x, y);
canvas.draw(textShape);
restore();
break;
case Paint.Style.FILL_AND_STROKE:
save();
translate(x, y);
canvas.draw(textShape);
canvas.fill(textShape);
restore();
break;
}
}
public void drawText(@NonNull String text, int start, int end, float x, float y,
@NonNull Paint paint) {
drawText(text.substring(start, end), x, y, paint);
}
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
String str = text.subSequence(start, end).toString();
drawText(str, x, y, paint);
}
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
throw new RuntimeException("Stub!");
}
public void drawRoundRect(float left, float top, float right, float bottom, float rx, float ry,
@NonNull Paint paint) {
throw new RuntimeException("Stub!");
}
public void drawPath(@NonNull Path path, @NonNull Paint paint) {
throw new RuntimeException("Stub!");
}
public void translate(float dx, float dy) {
if (dx == 0.0f && dy == 0.0f) return;
// TODO: check this, should translations stack?
canvas.translate(dx, dy);
}
public void scale(float sx, float sy) {
if (sx == 1.0f && sy == 1.0f) return;
canvas.scale(sx, sy);
}
public final void scale(float sx, float sy, float px, float py) {
if (sx == 1.0f && sy == 1.0f) return;
translate(px, py);
scale(sx, sy);
translate(-px, -py);
}
public void rotate(float degrees) {
if (degrees == 0.0f) return;
canvas.rotate(degrees);
}
public final void rotate(float degrees, float px, float py) {
if (degrees == 0.0f) return;
canvas.rotate(degrees, px, py);
}
public int getSaveCount() {
return transformStack.size();
}
public int save() {
transformStack.add(canvas.getTransform());
return getSaveCount();
}
public void restoreToCount(int saveCount) {
if (saveCount < 1) {
throw new IllegalArgumentException(
"Underflow in restoreToCount - more restores than saves");
}
if (saveCount > getSaveCount()) {
throw new IllegalArgumentException("Overflow in restoreToCount");
}
AffineTransform ts = transformStack.get(saveCount - 1);
canvas.setTransform(ts);
while (transformStack.size() >= saveCount) {
transformStack.remove(transformStack.size() - 1);
}
}
public void restore() {
restoreToCount(getSaveCount());
}
public boolean getClipBounds(@NonNull Rect bounds) {
Rectangle r = canvas.getClipBounds();
if (r == null) {
bounds.left = 0;
bounds.top = 0;
bounds.right = canvasImage.getWidth();
bounds.bottom = canvasImage.getHeight();
return true;
}
bounds.left = r.x;
bounds.top = r.y;
bounds.right = r.x + r.width;
bounds.bottom = r.y + r.height;
return r.width != 0 && r.height != 0;
}
public void drawColor(@ColorInt int colorInt) {
java.awt.Color color = Color.valueOf(colorInt).toJavaColor();
canvas.setColor(color);
canvas.fillRect(0, 0, canvasImage.getWidth(), canvasImage.getHeight());
}
public void drawColor(@ColorLong long colorLong) {
java.awt.Color color = Color.valueOf(colorLong).toJavaColor();
canvas.setColor(color);
canvas.fillRect(0, 0, canvasImage.getWidth(), canvasImage.getHeight());
}
public void drawPoint(float x, float y, Paint paint) {
applyPaint(paint);
Shape shape = paintToShape(paint, x, y);
if (paint.getStyle() == Paint.Style.FILL) {
canvas.fill(shape);
} else {
canvas.draw(shape);
}
}
private void applyPaint(Paint paint) {
canvas.setFont(paint.getTypeface().getFont());
java.awt.Color color = Color.valueOf(paint.getColorLong()).toJavaColor();
canvas.setColor(color);
canvas.setStroke(new BasicStroke(paint.getStrokeWidth(), paintToStrokeCap(paint), BasicStroke.JOIN_ROUND));
if (paint.isAntiAlias()) {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
} else {
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
}
if (paint.isDither()) {
canvas.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
} else {
canvas.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
}
// TODO: use more from paint?
}
private static int paintToStrokeCap(Paint paint) {
switch (paint.getStrokeCap()) {
case BUTT:
return BasicStroke.CAP_BUTT;
case SQUARE:
return BasicStroke.CAP_SQUARE;
case ROUND:
return BasicStroke.CAP_ROUND;
default:
throw new UnsupportedOperationException("Stroke cap " + paint.getStrokeCap() + " not supported");
}
}
private static Shape paintToShape(Paint paint, float x, float y) {
int width = (int)(paint.getStrokeWidth() * 2);
if (width <= 0) width = 1;
int upLeftX = (int) (x - (float) width / 2);
int upLeftY = (int) (y - (float) width / 2);
switch (paint.getStrokeCap()) {
case BUTT:
return new Rectangle((int)x, (int)y, 1, 1);
case SQUARE:
return new Rectangle(upLeftX, upLeftY, width, width);
case ROUND:
return new Ellipse2D.Float(upLeftX, upLeftY, width, width);
default:
throw new UnsupportedOperationException("Stroke cap " + paint.getStrokeCap() + " not supported");
}
} }
} }
@@ -0,0 +1,578 @@
/*
* 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.graphics;
import android.annotation.ColorInt;
import android.annotation.ColorLong;
import android.annotation.HalfFloat;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Size;
import android.util.Half;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.function.DoubleUnaryOperator;
public class Color {
@ColorInt public static final int BLACK = 0xFF000000;
@ColorInt public static final int DKGRAY = 0xFF444444;
@ColorInt public static final int GRAY = 0xFF888888;
@ColorInt public static final int LTGRAY = 0xFFCCCCCC;
@ColorInt public static final int WHITE = 0xFFFFFFFF;
@ColorInt public static final int RED = 0xFFFF0000;
@ColorInt public static final int GREEN = 0xFF00FF00;
@ColorInt public static final int BLUE = 0xFF0000FF;
@ColorInt public static final int YELLOW = 0xFFFFFF00;
@ColorInt public static final int CYAN = 0xFF00FFFF;
@ColorInt public static final int MAGENTA = 0xFFFF00FF;
@ColorInt public static final int TRANSPARENT = 0;
@NonNull
@Size(min = 4, max = 5)
private final float[] mComponents;
@NonNull
private final ColorSpace mColorSpace;
public Color() {
// This constructor is required for compatibility with previous APIs
mComponents = new float[] { 0.0f, 0.0f, 0.0f, 1.0f };
mColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
}
private Color(float r, float g, float b, float a) {
this(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
}
private Color(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
mComponents = new float[] { r, g, b, a };
mColorSpace = colorSpace;
}
private Color(@Size(min = 4, max = 5) float[] components, @NonNull ColorSpace colorSpace) {
mComponents = components;
mColorSpace = colorSpace;
}
public java.awt.Color toJavaColor() {
return new java.awt.Color(red(), green(), blue(), alpha());
}
@NonNull
public ColorSpace getColorSpace() {
return mColorSpace;
}
public ColorSpace.Model getModel() {
return mColorSpace.getModel();
}
public boolean isWideGamut() {
return getColorSpace().isWideGamut();
}
public boolean isSrgb() {
return getColorSpace().isSrgb();
}
@IntRange(from = 4, to = 5)
public int getComponentCount() {
return mColorSpace.getComponentCount() + 1;
}
@ColorLong
public long pack() {
return pack(mComponents[0], mComponents[1], mComponents[2], mComponents[3], mColorSpace);
}
@NonNull
public Color convert(@NonNull ColorSpace colorSpace) {
ColorSpace.Connector connector = ColorSpace.connect(mColorSpace, colorSpace);
float[] color = new float[] {
mComponents[0], mComponents[1], mComponents[2], mComponents[3]
};
connector.transform(color);
return new Color(color, colorSpace);
}
@ColorInt
public int toArgb() {
if (mColorSpace.isSrgb()) {
return ((int) (mComponents[3] * 255.0f + 0.5f) << 24) |
((int) (mComponents[0] * 255.0f + 0.5f) << 16) |
((int) (mComponents[1] * 255.0f + 0.5f) << 8) |
(int) (mComponents[2] * 255.0f + 0.5f);
}
float[] color = new float[] {
mComponents[0], mComponents[1], mComponents[2], mComponents[3]
};
// The transformation saturates the output
ColorSpace.connect(mColorSpace).transform(color);
return ((int) (color[3] * 255.0f + 0.5f) << 24) |
((int) (color[0] * 255.0f + 0.5f) << 16) |
((int) (color[1] * 255.0f + 0.5f) << 8) |
(int) (color[2] * 255.0f + 0.5f);
}
public float red() {
return mComponents[0];
}
public float green() {
return mComponents[1];
}
public float blue() {
return mComponents[2];
}
public float alpha() {
return mComponents[mComponents.length - 1];
}
@NonNull
@Size(min = 4, max = 5)
public float[] getComponents() {
return Arrays.copyOf(mComponents, mComponents.length);
}
@NonNull
@Size(min = 4)
public float[] getComponents(@Nullable @Size(min = 4) float[] components) {
if (components == null) {
return Arrays.copyOf(mComponents, mComponents.length);
}
if (components.length < mComponents.length) {
throw new IllegalArgumentException("The specified array's length must be at "
+ "least " + mComponents.length);
}
System.arraycopy(mComponents, 0, components, 0, mComponents.length);
return components;
}
public float getComponent(@IntRange(from = 0, to = 4) int component) {
return mComponents[component];
}
public float luminance() {
if (mColorSpace.getModel() != ColorSpace.Model.RGB) {
throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
"color space. The supplied color space is " + mColorSpace.getModel());
}
DoubleUnaryOperator eotf = ((ColorSpace.Rgb) mColorSpace).getEotf();
double r = eotf.applyAsDouble(mComponents[0]);
double g = eotf.applyAsDouble(mComponents[1]);
double b = eotf.applyAsDouble(mComponents[2]);
return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Color color = (Color) o;
//noinspection SimplifiableIfStatement
if (!Arrays.equals(mComponents, color.mComponents)) return false;
return mColorSpace.equals(color.mColorSpace);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(mComponents);
result = 31 * result + mColorSpace.hashCode();
return result;
}
@Override
@NonNull
public String toString() {
StringBuilder b = new StringBuilder("Color(");
for (float c : mComponents) {
b.append(c).append(", ");
}
b.append(mColorSpace.getName());
b.append(')');
return b.toString();
}
@NonNull
public static ColorSpace colorSpace(@ColorLong long color) {
return ColorSpace.get((int) (color & 0x3fL));
}
public static float red(@ColorLong long color) {
if ((color & 0x3fL) == 0L) return ((color >> 48) & 0xff) / 255.0f;
return Half.toFloat((short) ((color >> 48) & 0xffff));
}
public static float green(@ColorLong long color) {
if ((color & 0x3fL) == 0L) return ((color >> 40) & 0xff) / 255.0f;
return Half.toFloat((short) ((color >> 32) & 0xffff));
}
public static float blue(@ColorLong long color) {
if ((color & 0x3fL) == 0L) return ((color >> 32) & 0xff) / 255.0f;
return Half.toFloat((short) ((color >> 16) & 0xffff));
}
public static float alpha(@ColorLong long color) {
if ((color & 0x3fL) == 0L) return ((color >> 56) & 0xff) / 255.0f;
return ((color >> 6) & 0x3ff) / 1023.0f;
}
public static boolean isSrgb(@ColorLong long color) {
return colorSpace(color).isSrgb();
}
public static boolean isWideGamut(@ColorLong long color) {
return colorSpace(color).isWideGamut();
}
public static boolean isInColorSpace(@ColorLong long color, @NonNull ColorSpace colorSpace) {
return (int) (color & 0x3fL) == colorSpace.getId();
}
@ColorInt
public static int toArgb(@ColorLong long color) {
if ((color & 0x3fL) == 0L) return (int) (color >> 32);
float r = red(color);
float g = green(color);
float b = blue(color);
float a = alpha(color);
// The transformation saturates the output
float[] c = ColorSpace.connect(colorSpace(color)).transform(r, g, b);
return ((int) (a * 255.0f + 0.5f) << 24) |
((int) (c[0] * 255.0f + 0.5f) << 16) |
((int) (c[1] * 255.0f + 0.5f) << 8) |
(int) (c[2] * 255.0f + 0.5f);
}
@NonNull
public static Color valueOf(@ColorInt int color) {
float r = ((color >> 16) & 0xff) / 255.0f;
float g = ((color >> 8) & 0xff) / 255.0f;
float b = ((color ) & 0xff) / 255.0f;
float a = ((color >> 24) & 0xff) / 255.0f;
return new Color(r, g, b, a, ColorSpace.get(ColorSpace.Named.SRGB));
}
@NonNull
public static Color valueOf(@ColorLong long color) {
return new Color(red(color), green(color), blue(color), alpha(color), colorSpace(color));
}
@NonNull
public static Color valueOf(float r, float g, float b) {
return new Color(r, g, b, 1.0f);
}
@NonNull
public static Color valueOf(float r, float g, float b, float a) {
return new Color(saturate(r), saturate(g), saturate(b), saturate(a));
}
@NonNull
public static Color valueOf(float r, float g, float b, float a, @NonNull ColorSpace colorSpace) {
if (colorSpace.getComponentCount() > 3) {
throw new IllegalArgumentException("The specified color space must use a color model " +
"with at most 3 color components");
}
return new Color(r, g, b, a, colorSpace);
}
@NonNull
public static Color valueOf(@NonNull @Size(min = 4, max = 5) float[] components,
@NonNull ColorSpace colorSpace) {
if (components.length < colorSpace.getComponentCount() + 1) {
throw new IllegalArgumentException("Received a component array of length " +
components.length + " but the color model requires " +
(colorSpace.getComponentCount() + 1) + " (including alpha)");
}
return new Color(Arrays.copyOf(components, colorSpace.getComponentCount() + 1), colorSpace);
}
@ColorLong
public static long pack(@ColorInt int color) {
return (color & 0xffffffffL) << 32;
}
@ColorLong
public static long pack(float red, float green, float blue) {
return pack(red, green, blue, 1.0f, ColorSpace.get(ColorSpace.Named.SRGB));
}
@ColorLong
public static long pack(float red, float green, float blue, float alpha) {
return pack(red, green, blue, alpha, ColorSpace.get(ColorSpace.Named.SRGB));
}
@ColorLong
public static long pack(float red, float green, float blue, float alpha,
@NonNull ColorSpace colorSpace) {
if (colorSpace.isSrgb()) {
int argb =
((int) (alpha * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
return (argb & 0xffffffffL) << 32;
}
int id = colorSpace.getId();
if (id == ColorSpace.MIN_ID) {
throw new IllegalArgumentException(
"Unknown color space, please use a color space returned by ColorSpace.get()");
}
if (colorSpace.getComponentCount() > 3) {
throw new IllegalArgumentException(
"The color space must use a color model with at most 3 components");
}
@HalfFloat short r = Half.toHalf(red);
@HalfFloat short g = Half.toHalf(green);
@HalfFloat short b = Half.toHalf(blue);
int a = (int) (Math.max(0.0f, Math.min(alpha, 1.0f)) * 1023.0f + 0.5f);
// Suppress sign extension
return (r & 0xffffL) << 48 |
(g & 0xffffL) << 32 |
(b & 0xffffL) << 16 |
(a & 0x3ffL ) << 6 |
id & 0x3fL;
}
@ColorLong
public static long convert(@ColorInt int color, @NonNull ColorSpace colorSpace) {
float r = ((color >> 16) & 0xff) / 255.0f;
float g = ((color >> 8) & 0xff) / 255.0f;
float b = ((color ) & 0xff) / 255.0f;
float a = ((color >> 24) & 0xff) / 255.0f;
ColorSpace source = ColorSpace.get(ColorSpace.Named.SRGB);
return convert(r, g, b, a, source, colorSpace);
}
@ColorLong
public static long convert(@ColorLong long color, @NonNull ColorSpace colorSpace) {
float r = red(color);
float g = green(color);
float b = blue(color);
float a = alpha(color);
ColorSpace source = colorSpace(color);
return convert(r, g, b, a, source, colorSpace);
}
@ColorLong
public static long convert(float r, float g, float b, float a,
@NonNull ColorSpace source, @NonNull ColorSpace destination) {
float[] c = ColorSpace.connect(source, destination).transform(r, g, b);
return pack(c[0], c[1], c[2], a, destination);
}
@ColorLong
public static long convert(@ColorLong long color, @NonNull ColorSpace.Connector connector) {
float r = red(color);
float g = green(color);
float b = blue(color);
float a = alpha(color);
return convert(r, g, b, a, connector);
}
@ColorLong
public static long convert(float r, float g, float b, float a,
@NonNull ColorSpace.Connector connector) {
float[] c = connector.transform(r, g, b);
return pack(c[0], c[1], c[2], a, connector.getDestination());
}
public static float luminance(@ColorLong long color) {
ColorSpace colorSpace = colorSpace(color);
if (colorSpace.getModel() != ColorSpace.Model.RGB) {
throw new IllegalArgumentException("The specified color must be encoded in an RGB " +
"color space. The supplied color space is " + colorSpace.getModel());
}
DoubleUnaryOperator eotf = ((ColorSpace.Rgb) colorSpace).getEotf();
double r = eotf.applyAsDouble(red(color));
double g = eotf.applyAsDouble(green(color));
double b = eotf.applyAsDouble(blue(color));
return saturate((float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)));
}
private static float saturate(float v) {
return v <= 0.0f ? 0.0f : (v >= 1.0f ? 1.0f : v);
}
@IntRange(from = 0, to = 255)
public static int alpha(int color) {
return color >>> 24;
}
@IntRange(from = 0, to = 255)
public static int red(int color) {
return (color >> 16) & 0xFF;
}
@IntRange(from = 0, to = 255)
public static int green(int color) {
return (color >> 8) & 0xFF;
}
@IntRange(from = 0, to = 255)
public static int blue(int color) {
return color & 0xFF;
}
@ColorInt
public static int rgb(
@IntRange(from = 0, to = 255) int red,
@IntRange(from = 0, to = 255) int green,
@IntRange(from = 0, to = 255) int blue) {
return 0xff000000 | (red << 16) | (green << 8) | blue;
}
@ColorInt
public static int rgb(float red, float green, float blue) {
return 0xff000000 |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
@ColorInt
public static int argb(
@IntRange(from = 0, to = 255) int alpha,
@IntRange(from = 0, to = 255) int red,
@IntRange(from = 0, to = 255) int green,
@IntRange(from = 0, to = 255) int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}
@ColorInt
public static int argb(float alpha, float red, float green, float blue) {
return ((int) (alpha * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
public static float luminance(@ColorInt int color) {
ColorSpace.Rgb cs = (ColorSpace.Rgb) ColorSpace.get(ColorSpace.Named.SRGB);
DoubleUnaryOperator eotf = cs.getEotf();
double r = eotf.applyAsDouble(red(color) / 255.0);
double g = eotf.applyAsDouble(green(color) / 255.0);
double b = eotf.applyAsDouble(blue(color) / 255.0);
return (float) ((0.2126 * r) + (0.7152 * g) + (0.0722 * b));
}
@ColorInt
public static int parseColor(@Size(min=1) String colorString) {
if (colorString.charAt(0) == '#') {
// Use a long to avoid rollovers on #ffXXXXXX
long color = Long.parseLong(colorString.substring(1), 16);
if (colorString.length() == 7) {
// Set the alpha value
color |= 0x00000000ff000000;
} else if (colorString.length() != 9) {
throw new IllegalArgumentException("Unknown color");
}
return (int)color;
} else {
Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
if (color != null) {
return color;
}
}
throw new IllegalArgumentException("Unknown color");
}
public static void RGBToHSV(
@IntRange(from = 0, to = 255) int red,
@IntRange(from = 0, to = 255) int green,
@IntRange(from = 0, to = 255) int blue, @Size(3) float hsv[]) {
if (hsv.length < 3) {
throw new RuntimeException("3 components required for hsv");
}
nativeRGBToHSV(red, green, blue, hsv);
}
public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) {
RGBToHSV((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, hsv);
}
@ColorInt
public static int HSVToColor(@Size(3) float hsv[]) {
return HSVToColor(0xFF, hsv);
}
@ColorInt
public static int HSVToColor(@IntRange(from = 0, to = 255) int alpha, @Size(3) float hsv[]) {
if (hsv.length < 3) {
throw new RuntimeException("3 components required for hsv");
}
return nativeHSVToColor(alpha, hsv);
}
private static native void nativeRGBToHSV(int red, int greed, int blue, float hsv[]);
private static native int nativeHSVToColor(int alpha, float hsv[]);
private static final HashMap<String, Integer> sColorNameMap;
static {
sColorNameMap = new HashMap<>();
sColorNameMap.put("black", BLACK);
sColorNameMap.put("darkgray", DKGRAY);
sColorNameMap.put("gray", GRAY);
sColorNameMap.put("lightgray", LTGRAY);
sColorNameMap.put("white", WHITE);
sColorNameMap.put("red", RED);
sColorNameMap.put("green", GREEN);
sColorNameMap.put("blue", BLUE);
sColorNameMap.put("yellow", YELLOW);
sColorNameMap.put("cyan", CYAN);
sColorNameMap.put("magenta", MAGENTA);
sColorNameMap.put("aqua", 0xFF00FFFF);
sColorNameMap.put("fuchsia", 0xFFFF00FF);
sColorNameMap.put("darkgrey", DKGRAY);
sColorNameMap.put("grey", GRAY);
sColorNameMap.put("lightgrey", LTGRAY);
sColorNameMap.put("lime", 0xFF00FF00);
sColorNameMap.put("maroon", 0xFF800000);
sColorNameMap.put("navy", 0xFF000080);
sColorNameMap.put("olive", 0xFF808000);
sColorNameMap.put("purple", 0xFF800080);
sColorNameMap.put("silver", 0xFFC0C0C0);
sColorNameMap.put("teal", 0xFF008080);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,15 +1,15 @@
package android.graphics; package android.graphics;
import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import android.os.Parcel;
public final class Rect { public final class Rect {
int left; public int left;
int top; public int top;
int right; public int right;
int bottom; public int bottom;
private static final class UnflattenHelper { private static final class UnflattenHelper {
private static final Pattern FLATTENED_PATTERN = Pattern.compile( private static final Pattern FLATTENED_PATTERN = Pattern.compile(
@@ -37,11 +37,25 @@ public final class Rect {
this.right = 0; this.right = 0;
this.bottom = 0; this.bottom = 0;
} else { } else {
this.left = r.left;
this.top = r.top;
this.right = r.right;
this.bottom = r.bottom;
}
}
public void set(int left, int top, int right, int bottom) {
this.left = left; this.left = left;
this.top = top; this.top = top;
this.right = right; this.right = right;
this.bottom = bottom; this.bottom = bottom;
} }
public void set(Rect r) {
this.left = r.left;
this.top = r.top;
this.right = r.right;
this.bottom = r.bottom;
} }
public final int getWidth() { public final int getWidth() {
@@ -0,0 +1,50 @@
/*
* Copyright (C) 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.graphics;
import com.android.internal.util.ArrayUtils;
/**
* @hide
*/
public class TemporaryBuffer {
public static char[] obtain(int len) {
char[] buf;
synchronized (TemporaryBuffer.class) {
buf = sTemp;
sTemp = null;
}
if (buf == null || buf.length < len) {
buf = ArrayUtils.newUnpaddedCharArray(len);
}
return buf;
}
public static void recycle(char[] temp) {
if (temp.length > 1000) return;
synchronized (TemporaryBuffer.class) {
sTemp = temp;
}
}
private static char[] sTemp = null;
}
@@ -0,0 +1,422 @@
/*
* 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.graphics;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.content.res.AssetManager;
import android.util.Log;
import com.android.internal.util.Preconditions;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.font.TextAttribute;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import android.annotation.NonNull;
public class Typeface {
private static String TAG = "Typeface";
/** @hide */
public static final boolean ENABLE_LAZY_TYPEFACE_INITIALIZATION = true;
/** The default NORMAL typeface object */
public static final Typeface DEFAULT;
public static final Typeface DEFAULT_BOLD;
/** The NORMAL style of the default sans serif typeface. */
public static final Typeface SANS_SERIF;
/** The NORMAL style of the default serif typeface. */
public static final Typeface SERIF;
/** The NORMAL style of the default monospace typeface. */
public static final Typeface MONOSPACE;
public @interface Style {}
// Style
public static final int NORMAL = 0;
public static final int BOLD = 1;
public static final int ITALIC = 2;
public static final int BOLD_ITALIC = 3;
/** @hide */ public static final int STYLE_MASK = 0x03;
public static final String DEFAULT_FAMILY = "sans-serif";
private final Font mFont;
private final List<Font> mFallbackFonts;
/** Returns the typeface's weight value */
public int getWeight() {
Map<TextAttribute, Object> atts = (Map<TextAttribute, Object>) mFont.getAttributes();
Object weight = atts.getOrDefault(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR);
if (weight instanceof Float) {
float w = ((Float) weight).floatValue();
// undo the transformation
return (int) ((w - TextAttribute.WEIGHT_REGULAR) / (TextAttribute.WEIGHT_BOLD - TextAttribute.WEIGHT_REGULAR) * (Builder.BOLD_WEIGHT - Builder.NORMAL_WEIGHT) + Builder.NORMAL_WEIGHT);
}
return Builder.NORMAL_WEIGHT;
}
public float getJavaWeight() {
Map<TextAttribute, Object> atts = (Map<TextAttribute, Object>) mFont.getAttributes();
Object weight = atts.getOrDefault(TextAttribute.WEIGHT, TextAttribute.WEIGHT_REGULAR);
if (weight instanceof Float) {
return ((Float) weight).floatValue();
}
return TextAttribute.WEIGHT_REGULAR;
}
/** Returns the typeface's intrinsic style attributes */
public @Style int getStyle() {
if (isBold() && isItalic()) return BOLD_ITALIC;
if (isBold()) return BOLD;
if (isItalic()) return ITALIC;
return NORMAL;
}
/** Returns true if getStyle() has the BOLD bit set. */
public final boolean isBold() {
return mFont.isBold();
}
/** Returns true if getStyle() has the ITALIC bit set. */
public final boolean isItalic() {
return mFont.isItalic();
}
public final @Nullable String getSystemFontFamilyName() {
return mFont.getFamily();
}
public static Typeface findFromCache(AssetManager mgr, String path) {
throw new RuntimeException("Stub!");
}
public static final class Builder {
/** @hide */
public static final int NORMAL_WEIGHT = 400;
/** @hide */
public static final int BOLD_WEIGHT = 700;
private final String mPath;
private Font mFont;
private int mStyle;
private Map<TextAttribute, Object> mAttributes;
private String mFallbackFamilyName;
public Builder(@NonNull File path) {
mFont = loadFont(path);
Log.v(TAG, "Font loaded from " + path.toURI());
mPath = null;
mStyle = 0;
mAttributes = new HashMap<TextAttribute, Object>();
}
public Builder(@NonNull FileDescriptor fd) {
throw new RuntimeException("Stub!");
}
public Builder(@NonNull String path) {
mFont = loadFont(new File(path));
mPath = path;
mStyle = 0;
mAttributes = new HashMap<TextAttribute, Object>();
}
public Builder(@NonNull AssetManager assetManager, @NonNull String path) {
throw new RuntimeException("Stub!");
}
public Builder(@NonNull AssetManager assetManager, @NonNull String path, boolean isAsset,
int cookie) {
throw new RuntimeException("Stub!");
}
public Builder setWeight(int weight) {
// java font weight does not follow typical weight distribution
// In Java, regular weight is at 1.0 and bold at 2.0, compared to 400 and 700 in TTF
// Typical range is 0 to 1000
float jWeight = (weight - NORMAL_WEIGHT) / (BOLD_WEIGHT - NORMAL_WEIGHT) * (TextAttribute.WEIGHT_BOLD - TextAttribute.WEIGHT_REGULAR) + TextAttribute.WEIGHT_REGULAR;
mAttributes.put(TextAttribute.WEIGHT, jWeight);
return this;
}
public Builder setItalic(boolean italic) {
if (italic) {
mStyle |= Font.ITALIC;
} else {
mStyle &= ~Font.ITALIC;
}
return this;
}
public Builder setTtcIndex(int ttcIndex) {
throw new RuntimeException("Stub!");
}
public Builder setFontVariationSettings(@Nullable String variationSettings) {
throw new RuntimeException("Stub!");
}
public Builder setFallback(@Nullable String familyName) {
mFallbackFamilyName = familyName;
return this;
}
private Typeface resolveFallbackTypeface() {
if (mFallbackFamilyName == null) {
return null;
}
return new Typeface(new Font(mFallbackFamilyName, mStyle, 12).deriveFont(mAttributes));
}
public Typeface build() {
if (mFont == null) return resolveFallbackTypeface();
return new Typeface(mFont.deriveFont(mStyle).deriveFont(mAttributes));
}
private Font loadFont(File fontFile) {
try {
return Font.createFont(Font.TRUETYPE_FONT, fontFile);
} catch (FontFormatException ex) {
Log.v(TAG, "Failed to create font as TTF", ex);
} catch (IOException ex) {
Log.v(TAG, "Failed to create font as TTF", ex);
throw new RuntimeException(ex);
}
try {
return Font.createFont(Font.TYPE1_FONT, fontFile);
} catch (FontFormatException ex) {
Log.v(TAG, "Failed to create font as T1", ex);
} catch (IOException ex) {
Log.v(TAG, "Failed to create font as T1", ex);
throw new RuntimeException(ex);
}
return null;
}
}
public static Typeface create(String familyName, @Style int style) {
return create(getSystemDefaultTypeface(familyName), style);
}
public static Typeface create(Typeface family, @Style int style) {
if ((style & ~STYLE_MASK) != 0) {
style = NORMAL;
}
if (family == null) {
family = getSystemDefaultTypeface(DEFAULT_FAMILY);
}
throw new RuntimeException("Stub!");
}
public static @NonNull Typeface create(@Nullable Typeface family,
int weight, boolean italic) {
Preconditions.checkArgumentInRange(weight, 0, 1000, "weight");
if (family == null) {
family = getSystemDefaultTypeface(DEFAULT_FAMILY);
}
return createWeightStyle(family, weight, italic);
}
private static @NonNull Typeface createWeightStyle(@NonNull Typeface base,
int weight, boolean italic) {
throw new RuntimeException("Stub!");
}
public static Typeface defaultFromStyle(@Style int style) {
throw new RuntimeException("Stub!");
}
public static Typeface createFromAsset(AssetManager mgr, String path) {
Preconditions.checkNotNull(path); // for backward compatibility
Preconditions.checkNotNull(mgr);
Typeface typeface = new Builder(mgr, path).build();
if (typeface != null) return typeface;
// check if the file exists, and throw an exception for backward compatibility
try (InputStream inputStream = mgr.open(path)) {
} catch (IOException e) {
throw new RuntimeException("Font asset not found " + path);
}
return Typeface.DEFAULT;
}
public Typeface(Font fnt) {
mFont = fnt;
mFallbackFonts = Collections.emptyList();
}
public Typeface(Font fnt, List<Font> fallbackFonts) {
mFont = fnt;
mFallbackFonts = fallbackFonts;
}
public Map<TextAttribute, Object> getAttributes() {
return (Map<TextAttribute, Object>) mFont.getAttributes();
}
public Typeface deriveFont(Map<TextAttribute, Object> attributes) {
Font mainFont = mFont.deriveFont(attributes);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(attributes))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public Typeface deriveFont(float size) {
Font mainFont = mFont.deriveFont(size);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(size))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public Typeface deriveFont(int style, float size) {
Font mainFont = mFont.deriveFont(style, size);
List<Font> fallbacks = mFallbackFonts.stream().map(font -> font.deriveFont(style, size))
.collect(Collectors.toList());
return new Typeface(mainFont, fallbacks);
}
public AttributedString createWithFallback(String text) {
AttributedString result = new AttributedString(text);
int textLength = text.length();
result.addAttribute(TextAttribute.FONT, mFont, 0, textLength);
int i = 0;
while (true) {
int until = mFont.canDisplayUpTo(result.getIterator(), i, textLength);
if (until == -1) break;
boolean found = false;
// find a fallback font from `until`
for (int j = 0; j < mFallbackFonts.size(); ++j) {
int fallbackUntil = until;
for (; fallbackUntil < textLength; ++fallbackUntil) {
if (mFont.canDisplay(text.charAt(fallbackUntil)) || !mFallbackFonts.get(j).canDisplay(text.charAt(fallbackUntil)))
break;
}
if (fallbackUntil > until) {
// use this and advance
int end = fallbackUntil >= 0 ? fallbackUntil : textLength;
result.addAttribute(TextAttribute.FONT, mFallbackFonts.get(j), until, end);
Log.v(TAG, String.format("Fallback: from %d to %d using %s", until, end, mFallbackFonts.get(j).getName()));
i = end;
found = true;
break;
}
}
if (found) continue;
Log.w(TAG, String.format("No fallback font found at %d, skipping", until));
i = until + 1;
}
return result;
}
public static Typeface createFromFile(@Nullable File file) {
// For the compatibility reasons, leaving possible NPE here.
// See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
Typeface typeface = new Builder(file).build();
if (typeface != null) return typeface;
// check if the file exists, and throw an exception for backward compatibility
if (!file.exists()) {
throw new RuntimeException("Font asset not found " + file.getAbsolutePath());
}
return Typeface.DEFAULT;
}
public static Typeface createFromFile(@Nullable String path) {
Preconditions.checkNotNull(path); // for backward compatibility
return createFromFile(new File(path));
}
private static Typeface getSystemDefaultTypeface(@NonNull String familyName) {
return new Typeface(new Font(familyName, 0, 12));
}
public Font getFont() {
return mFont;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Typeface typeface = (Typeface) o;
return typeface.mFont.equals(mFont);
}
@Override
public int hashCode() {
return mFont.hashCode();
}
private static Font loadFontAsset(String font) {
try (InputStream defaultNormalStream = ClassLoader.getSystemClassLoader().getResourceAsStream("font/" + font)) {
return Font.createFont(Font.TRUETYPE_FONT, defaultNormalStream).deriveFont(12.0f);
} catch (Exception ex) {
Log.e(TAG, "Failed to load " + font, ex);
return null;
}
}
private static Typeface withFallback(Font baseFallback, String mainFont, String... fonts) {
Font main = loadFontAsset(mainFont);
if (main == null) main = new Font(null, 0, 12);
List<Font> fallbacks = Stream.concat(Arrays.stream(fonts).map(Typeface::loadFontAsset).filter(f -> f != null), Stream.of(baseFallback))
.collect(Collectors.toList());
Log.v(TAG, String.format("Loaded font %s with %d fallback fonts", main.getName(), fallbacks.size()));
return new Typeface(main, fallbacks);
}
static {
DEFAULT = withFallback(new Font(null, 0, 12), "NotoSans/NotoSans-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
DEFAULT_BOLD = DEFAULT.deriveFont(Font.BOLD);
SANS_SERIF = DEFAULT;
SERIF = withFallback(new Font(Font.SERIF, 0, 12), "NotoSans/NotoSerif-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
MONOSPACE = withFallback(new Font(Font.MONOSPACED, 0, 12), "NotoSans/NotoSansMono-VariableFont_wdth,wght.ttf", "NotoSans/NotoSansSymbols2-Regular.ttf", "NotoSans/NotoEmoji-VariableFont_wght.ttf");
}
}
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
@@ -42,19 +42,19 @@ public class SystemProperties {
return native_get(key); return native_get(key);
} }
private static int native_get_int(String key, int def) { private static int native_get_int(String key, int def) {
if(configModule.hasProperty(key)) if(!configModule.hasProperty(key))
return def; return def;
else else
return configModule.getIntProperty(key); return configModule.getIntProperty(key);
} }
private static long native_get_long(String key, long def) { private static long native_get_long(String key, long def) {
if(configModule.hasProperty(key)) if(!configModule.hasProperty(key))
return def; return def;
else else
return configModule.getLongProperty(key); return configModule.getLongProperty(key);
} }
private static boolean native_get_boolean(String key, boolean def) { private static boolean native_get_boolean(String key, boolean def) {
if(configModule.hasProperty(key)) if(!configModule.hasProperty(key))
return def; return def;
else else
return configModule.getBooleanProperty(key); return configModule.getBooleanProperty(key);
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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.text;
import android.annotation.ColorInt;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.Log;
import java.awt.RenderingHints;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.text.AttributedString;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
public class StaticLayout extends Layout {
/*
* The break iteration is done in native code. The protocol for using the native code is as
* follows.
*
* First, call nInit to setup native line breaker object. Then, for each paragraph, do the
* following:
*
* - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
* native.
* - Run LineBreaker.computeLineBreaks() to obtain line breaks for the paragraph.
*
* After all paragraphs, call finish() to release expensive buffers.
*/
static final String TAG = "StaticLayout";
public final static class Builder {
private Builder() {}
@NonNull
public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start,
@IntRange(from = 0) int end, @NonNull TextPaint paint,
@IntRange(from = 0) int width) {
Builder b = new Builder();
// set default initial values
b.mText = source;
b.mStart = start;
b.mEnd = end;
b.mPaint = paint;
b.mWidth = width;
b.mAlignment = Alignment.ALIGN_NORMAL;
b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
b.mIncludePad = true;
b.mFallbackLineSpacing = false;
b.mEllipsizedWidth = width;
b.mEllipsize = null;
b.mMaxLines = Integer.MAX_VALUE;
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
b.mMinimumFontMetrics = null;
return b;
}
// release any expensive state
/* package */ void finish() {
mText = null;
mPaint = null;
mLeftIndents = null;
mRightIndents = null;
mMinimumFontMetrics = null;
}
public Builder setText(CharSequence source) {
return setText(source, 0, source.length());
}
@NonNull
public Builder setText(@NonNull CharSequence source, int start, int end) {
mText = source;
mStart = start;
mEnd = end;
return this;
}
@NonNull
public Builder setPaint(@NonNull TextPaint paint) {
mPaint = paint;
return this;
}
@NonNull
public Builder setWidth(@IntRange(from = 0) int width) {
mWidth = width;
if (mEllipsize == null) {
mEllipsizedWidth = width;
}
return this;
}
@NonNull
public Builder setAlignment(@NonNull Alignment alignment) {
mAlignment = alignment;
return this;
}
@NonNull
public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
mTextDir = textDir;
return this;
}
@NonNull
public Builder setLineSpacing(float spacingAdd, float spacingMult) {
mSpacingAdd = spacingAdd;
mSpacingMult = spacingMult;
return this;
}
@NonNull
public Builder setIncludePad(boolean includePad) {
mIncludePad = includePad;
return this;
}
@NonNull
public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
mFallbackLineSpacing = useLineSpacingFromFallbacks;
return this;
}
@NonNull
public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) {
mEllipsizedWidth = ellipsizedWidth;
return this;
}
@NonNull
public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) {
mEllipsize = ellipsize;
return this;
}
@NonNull
public Builder setMaxLines(@IntRange(from = 0) int maxLines) {
mMaxLines = maxLines;
return this;
}
@NonNull
public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
mBreakStrategy = breakStrategy;
return this;
}
@NonNull
public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
mHyphenationFrequency = hyphenationFrequency;
return this;
}
@NonNull
public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) {
mLeftIndents = leftIndents;
mRightIndents = rightIndents;
return this;
}
@NonNull
public Builder setJustificationMode(@JustificationMode int justificationMode) {
mJustificationMode = justificationMode;
return this;
}
@NonNull
/* package */ Builder setAddLastLineLineSpacing(boolean value) {
mAddLastLineLineSpacing = value;
return this;
}
@SuppressLint("MissingGetterMatchingBuilder") // The base class `Layout` has a getter.
@NonNull
public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
mUseBoundsForWidth = useBoundsForWidth;
return this;
}
@NonNull
// The corresponding getter is getShiftDrawingOffsetForStartOverhang()
@SuppressLint("MissingGetterMatchingBuilder")
public Builder setShiftDrawingOffsetForStartOverhang(
boolean shiftDrawingOffsetForStartOverhang) {
mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
return this;
}
public Builder setCalculateBounds(boolean value) {
mCalculateBounds = value;
return this;
}
@NonNull
public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
mMinimumFontMetrics = minimumFontMetrics;
return this;
}
@NonNull
public StaticLayout build() {
StaticLayout result = new StaticLayout(this, mIncludePad, mEllipsize != null
? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
return result;
}
private CharSequence mText;
private int mStart;
private int mEnd;
private TextPaint mPaint;
private int mWidth;
private Alignment mAlignment;
private TextDirectionHeuristic mTextDir;
private float mSpacingMult;
private float mSpacingAdd;
private boolean mIncludePad;
private boolean mFallbackLineSpacing;
private int mEllipsizedWidth;
private TextUtils.TruncateAt mEllipsize;
private int mMaxLines;
private int mBreakStrategy;
private int mHyphenationFrequency;
@Nullable private int[] mLeftIndents;
@Nullable private int[] mRightIndents;
private int mJustificationMode;
private boolean mAddLastLineLineSpacing;
private boolean mUseBoundsForWidth;
private boolean mShiftDrawingOffsetForStartOverhang;
private boolean mCalculateBounds;
@Nullable private Paint.FontMetrics mMinimumFontMetrics;
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
}
private StaticLayout() {
super(
null, // text
null, // paint
0, // width
null, // alignment
null, // textDir
1, // spacing multiplier
0, // spacing amount
false, // include font padding
false, // fallback line spacing
0, // ellipsized width
null, // ellipsize
1, // maxLines
BREAK_STRATEGY_SIMPLE,
HYPHENATION_FREQUENCY_NONE,
null, // leftIndents
null, // rightIndents
JUSTIFICATION_MODE_NONE,
false, // useBoundsForWidth
false, // shiftDrawingOffsetForStartOverhang
null // minimumFontMetrics
);
mColumns = COLUMNS_ELLIPSIZE;
mLineDirections = new Directions[2];
mLines = new int[2 * mColumns];
}
@Deprecated
public StaticLayout(CharSequence source, TextPaint paint,
int width,
Alignment align, float spacingmult, float spacingadd,
boolean includepad) {
this(source, 0, source.length(), paint, width, align,
spacingmult, spacingadd, includepad);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
boolean includepad) {
this(source, bufstart, bufend, paint, outerwidth, align,
spacingmult, spacingadd, includepad, null, 0);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
this(source, bufstart, bufend, paint, outerwidth, align,
TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE);
}
@Deprecated
public StaticLayout(CharSequence source, int bufstart, int bufend,
TextPaint paint, int outerwidth,
Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
boolean includepad,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth, int maxLines) {
this(Builder.obtain(source, bufstart, bufend, paint, outerwidth)
.setAlignment(align)
.setTextDirection(textDir)
.setLineSpacing(spacingadd, spacingmult)
.setIncludePad(includepad)
.setEllipsize(ellipsize)
.setEllipsizedWidth(ellipsizedWidth)
.setMaxLines(maxLines), includepad,
ellipsize != null ? COLUMNS_ELLIPSIZE : COLUMNS_NORMAL);
}
private StaticLayout(Builder b, boolean trackPadding, int columnSize) {
super((b.mEllipsize == null) ? b.mText : (b.mText instanceof Spanned)
? new SpannedEllipsizer(b.mText) : new Ellipsizer(b.mText),
b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents,
b.mRightIndents, b.mJustificationMode, b.mUseBoundsForWidth,
b.mShiftDrawingOffsetForStartOverhang, b.mMinimumFontMetrics);
mColumns = columnSize;
if (b.mEllipsize != null) {
Ellipsizer e = (Ellipsizer) getText();
e.mLayout = this;
e.mWidth = b.mEllipsizedWidth;
e.mMethod = b.mEllipsize;
throw new UnsupportedOperationException("Ellipsis not supported");
}
mLineDirections = new Directions[2];
mLines = new int[2 * mColumns];
mMaximumVisibleLineCount = b.mMaxLines;
mLeftIndents = b.mLeftIndents;
mRightIndents = b.mRightIndents;
String str = b.mText.subSequence(b.mStart, b.mEnd).toString();
AttributedString text = getPaint().getTypeface().createWithFallback(str);
FontRenderContext frc = new FontRenderContext(null, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
LineBreakMeasurer measurer = new LineBreakMeasurer(text.getIterator(), frc);
// TODO: directions
float y = 0;
while (measurer.getPosition() < str.length()) {
int off = mLineCount * mColumns;
int want = off + mColumns + TOP;
if (want >= mLines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
System.arraycopy(mLines, 0, grow, 0, mLines.length);
mLines = grow;
}
int pos = measurer.getPosition();
TextLayout l = measurer.nextLayout(getWidth());
mLines[off + START] = pos;
mLines[off + TOP] = (int) y;
mLines[off + DESCENT] = (int) (l.getDescent() + l.getLeading());
mLines[off + EXTRA] = (int) l.getLeading();
mLines[off + DIR] |= Layout.DIR_LEFT_TO_RIGHT << DIR_SHIFT;
y += l.getAscent();
y += l.getDescent() + l.getLeading();
mLines[off + mColumns + START] = measurer.getPosition();
mLines[off + mColumns + TOP] = (int) y;
mLineCount += 1;
}
}
// Override the base class so we can directly access our members,
// rather than relying on member functions.
// The logic mirrors that of Layout.getLineForVertical
// FIXME: It may be faster to do a linear search for layouts without many lines.
@Override
public int getLineForVertical(int vertical) {
int high = mLineCount;
int low = -1;
int guess;
int[] lines = mLines;
while (high - low > 1) {
guess = (high + low) >> 1;
if (lines[mColumns * guess + TOP] > vertical){
high = guess;
} else {
low = guess;
}
}
if (low < 0) {
return 0;
} else {
return low;
}
}
@Override
public int getLineCount() {
return mLineCount;
}
@Override
public int getLineTop(int line) {
return mLines[mColumns * line + TOP];
}
@Override
public int getLineExtra(int line) {
return mLines[mColumns * line + EXTRA];
}
@Override
public int getLineDescent(int line) {
return mLines[mColumns * line + DESCENT];
}
@Override
public int getLineStart(int line) {
return mLines[mColumns * line + START] & START_MASK;
}
@Override
public int getParagraphDirection(int line) {
return mLines[mColumns * line + DIR] >> DIR_SHIFT;
}
@Override
public boolean getLineContainsTab(int line) {
return (mLines[mColumns * line + TAB] & TAB_MASK) != 0;
}
@Override
public final Directions getLineDirections(int line) {
if (line > getLineCount()) {
throw new ArrayIndexOutOfBoundsException();
}
return new Directions(null);
// return mLineDirections[line];
}
@Override
public int getTopPadding() {
return mTopPadding;
}
@Override
public int getBottomPadding() {
return mBottomPadding;
}
// To store into single int field, pack the pair of start and end hyphen edit.
static int packHyphenEdit(
@Paint.StartHyphenEdit int start, @Paint.EndHyphenEdit int end) {
return start << START_HYPHEN_BITS_SHIFT | end;
}
static int unpackStartHyphenEdit(int packedHyphenEdit) {
return (packedHyphenEdit & START_HYPHEN_MASK) >> START_HYPHEN_BITS_SHIFT;
}
static int unpackEndHyphenEdit(int packedHyphenEdit) {
return packedHyphenEdit & END_HYPHEN_MASK;
}
@Override
public @Paint.StartHyphenEdit int getStartHyphenEdit(int lineNumber) {
return unpackStartHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
}
@Override
public @Paint.EndHyphenEdit int getEndHyphenEdit(int lineNumber) {
return unpackEndHyphenEdit(mLines[mColumns * lineNumber + HYPHEN] & HYPHEN_MASK);
}
@Override
public int getIndentAdjust(int line, Alignment align) {
if (align == Alignment.ALIGN_LEFT) {
if (mLeftIndents == null) {
return 0;
} else {
return mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
}
} else if (align == Alignment.ALIGN_RIGHT) {
if (mRightIndents == null) {
return 0;
} else {
return -mRightIndents[Math.min(line, mRightIndents.length - 1)];
}
} else if (align == Alignment.ALIGN_CENTER) {
int left = 0;
if (mLeftIndents != null) {
left = mLeftIndents[Math.min(line, mLeftIndents.length - 1)];
}
int right = 0;
if (mRightIndents != null) {
right = mRightIndents[Math.min(line, mRightIndents.length - 1)];
}
return (left - right) >> 1;
} else {
throw new AssertionError("unhandled alignment " + align);
}
}
@Override
public int getEllipsisCount(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;
}
return mLines[mColumns * line + ELLIPSIS_COUNT];
}
@Override
public int getEllipsisStart(int line) {
if (mColumns < COLUMNS_ELLIPSIZE) {
return 0;
}
return mLines[mColumns * line + ELLIPSIS_START];
}
@Override
@NonNull
public RectF computeDrawingBoundingBox() {
// Cache the drawing bounds result because it does not change after created.
if (mDrawingBounds == null) {
mDrawingBounds = super.computeDrawingBoundingBox();
}
return mDrawingBounds;
}
@Override
public int getHeight(boolean cap) {
if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
&& Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "maxLineHeight should not be -1. "
+ " maxLines:" + mMaximumVisibleLineCount
+ " lineCount:" + mLineCount);
}
return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
? mMaxLineHeight : super.getHeight();
}
private int mLineCount;
private int mTopPadding, mBottomPadding;
private int mColumns;
private RectF mDrawingBounds = null; // lazy calculation.
private boolean mEllipsized;
private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
private static final int COLUMNS_NORMAL = 5;
private static final int COLUMNS_ELLIPSIZE = 7;
private static final int START = 0;
private static final int DIR = START;
private static final int TAB = START;
private static final int TOP = 1;
private static final int DESCENT = 2;
private static final int EXTRA = 3;
private static final int HYPHEN = 4;
private static final int ELLIPSIS_START = 5;
private static final int ELLIPSIS_COUNT = 6;
private int[] mLines;
private Directions[] mLineDirections;
private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
private static final int START_MASK = 0x1FFFFFFF;
private static final int DIR_SHIFT = 30;
private static final int TAB_MASK = 0x20000000;
private static final int HYPHEN_MASK = 0xFF;
private static final int START_HYPHEN_BITS_SHIFT = 3;
private static final int START_HYPHEN_MASK = 0x18; // 0b11000
private static final int END_HYPHEN_MASK = 0x7; // 0b00111
private static final float TAB_INCREMENT = 20; // same as Layout, but that's private
private static final char CHAR_NEW_LINE = '\n';
private static final double EXTRA_ROUNDING = 0.5;
private static final int DEFAULT_MAX_LINE_HEIGHT = -1;
// Unused, here because of gray list private API accesses.
/*package*/ static class LineBreaks {
private static final int INITIAL_SIZE = 16;
public int[] breaks = new int[INITIAL_SIZE];
public float[] widths = new float[INITIAL_SIZE];
public float[] ascents = new float[INITIAL_SIZE];
public float[] descents = new float[INITIAL_SIZE];
public int[] flags = new int[INITIAL_SIZE]; // hasTab
// breaks, widths, and flags should all have the same length
}
@Nullable private int[] mLeftIndents;
@Nullable private int[] mRightIndents;
}
@@ -0,0 +1,182 @@
/*
* Copyright (C) 2010 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.text;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout.Directions;
import android.text.Layout.TabStops;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.TextMeasurer;
import java.text.AttributedString;
public class TextLine {
private TextPaint mPaint;
private CharSequence mText;
private int mStart;
private int mLen;
private int mDir;
private Directions mDirections;
private boolean mHasTabs;
private TabStops mTabs;
private char[] mChars;
private boolean mCharsValid;
private Spanned mSpanned;
private PrecomputedText mComputed;
private RectF mTmpRectForMeasure;
private RectF mTmpRectForPaintAPI;
private Rect mTmpRectForPrecompute;
public static final class LineInfo {
private int mClusterCount;
public int getClusterCount() {
return mClusterCount;
}
public void setClusterCount(int clusterCount) {
mClusterCount = clusterCount;
}
};
public float getAddedWordSpacingInPx() {
throw new RuntimeException("Stub!");
}
public float getAddedLetterSpacingInPx() {
throw new RuntimeException("Stub!");
}
public boolean isJustifying() {
throw new RuntimeException("Stub!");
}
/** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
private static final TextLine[] sCached = new TextLine[3];
public static TextLine obtain() {
TextLine tl;
synchronized (sCached) {
for (int i = sCached.length; --i >= 0;) {
if (sCached[i] != null) {
tl = sCached[i];
sCached[i] = null;
return tl;
}
}
}
tl = new TextLine();
return tl;
}
public static TextLine recycle(TextLine tl) {
synchronized(sCached) {
for (int i = 0; i < sCached.length; ++i) {
if (sCached[i] == null) {
sCached[i] = tl;
break;
}
}
}
return null;
}
public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Directions directions, boolean hasTabs, TabStops tabStops,
int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing) {
mPaint = paint;
mText = text;
mStart = start;
mLen = limit - start;
mDir = dir;
mDirections = directions;
if (mDirections == null) {
throw new IllegalArgumentException("Directions cannot be null");
}
mHasTabs = hasTabs;
mSpanned = null;
if (text instanceof Spanned) {
mSpanned = (Spanned) text;
}
mComputed = null;
if (text instanceof PrecomputedText) {
// Here, no need to check line break strategy or hyphenation frequency since there is no
// line break concept here.
mComputed = (PrecomputedText) text;
if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
mComputed = null;
}
}
mTabs = tabStops;
}
public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
throw new RuntimeException("Stub!");
}
public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) {
throw new RuntimeException("Stub!");
}
public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart,
int runEnd, int spanStart, int spanEnd) {
throw new RuntimeException("Stub!");
}
public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
@Nullable LineInfo lineInfo) {
FontRenderContext frc = new FontRenderContext(null, RenderingHints.VALUE_TEXT_ANTIALIAS_DEFAULT, RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
AttributedString text = mPaint.getTypeface().createWithFallback(mText.toString());
TextMeasurer tm = new TextMeasurer(text.getIterator(), frc);
return (float) tm.getLayout(mStart, mStart + mLen).getBounds().getWidth();
}
public float measure(@IntRange(from = 0) int offset, boolean trailing,
@NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
throw new RuntimeException("Stub!");
}
public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) {
throw new RuntimeException("Stub!");
}
public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
throw new RuntimeException("Stub!");
}
// Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
public static boolean isLineEndSpace(char ch) {
return ch == ' ' || ch == '\t' || ch == 0x1680
|| (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
|| ch == 0x205F || ch == 0x3000;
}
void draw(Canvas c, float x, int top, int y, int bottom) {
c.drawText(mText, mStart, mStart + mLen, x, y, mPaint);
}
}
@@ -0,0 +1,74 @@
/*
* 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.text;
import android.annotation.ColorInt;
import android.graphics.Paint;
public class TextPaint extends Paint {
// Special value 0 means no background paint
@ColorInt
public int bgColor;
public int baselineShift;
@ColorInt
public int linkColor;
public int[] drawableState;
public float density = 1.0f;
@ColorInt
public int underlineColor = 0;
public float underlineThickness;
public TextPaint() {
super();
}
public TextPaint(int flags) {
super(flags);
}
public TextPaint(Paint p) {
super(p);
}
public void set(TextPaint tp) {
super.set(tp);
bgColor = tp.bgColor;
baselineShift = tp.baselineShift;
linkColor = tp.linkColor;
drawableState = tp.drawableState;
density = tp.density;
underlineColor = tp.underlineColor;
underlineThickness = tp.underlineThickness;
}
public void setUnderlineText(int color, float thickness) {
underlineColor = color;
underlineThickness = thickness;
}
@Override
public float getUnderlineThickness() {
if (underlineColor != 0) { // Return custom thickness only if underline color is set.
return underlineThickness;
} else {
return super.getUnderlineThickness();
}
}
}
@@ -6,6 +6,7 @@
package android.util; package android.util;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.event.Level;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.PrintWriter; import java.io.PrintWriter;
@@ -92,7 +93,7 @@ public final class Log {
} }
private static int log(int level, String tag, String msg) { private static int log(int level, String tag, String msg) {
logger.info(formatLog(level, tag, msg)); logger.atLevel(intToLevel(level)).log(formatLog(tag, msg));
return tag.length() + msg.length(); //Not accurate, but never used anyways return tag.length() + msg.length(); //Not accurate, but never used anyways
} }
@@ -101,39 +102,35 @@ public final class Log {
} }
private static int log(int level, String tag, String msg, Throwable t) { private static int log(int level, String tag, String msg, Throwable t) {
logger.info(formatLog(level, tag, msg), t); logger.atLevel(intToLevel(level)).setCause(t).log(formatLog(tag, msg));
return tag.length() + msg.length(); //Not accurate, but never used anyways return tag.length() + msg.length(); //Not accurate, but never used anyways
} }
private static String formatLog(int level, String tag, String msg) { private static String formatLog(String tag, String msg) {
StringBuilder first = new StringBuilder("["); StringBuilder first = new StringBuilder("[");
switch(level) {
case ASSERT:
first.append("ASSERT");
break;
case DEBUG:
first.append("DEBUG");
break;
case ERROR:
first.append("ERROR");
break;
case INFO:
first.append("INFO");
break;
case VERBOSE:
first.append("VERBOSE");
break;
case WARN:
first.append("WARN");
break;
default:
first.append("UNKNOWN");
break;
}
first.append("] ");
first.append(tag); first.append(tag);
first.append(": "); first.append("]: ");
first.append(msg); first.append(msg);
return first.toString(); return first.toString();
} }
private static Level intToLevel(int level) {
switch(level) {
case ASSERT:
return Level.ERROR;
case DEBUG:
return Level.DEBUG;
case ERROR:
return Level.ERROR;
case INFO:
return Level.INFO;
case VERBOSE:
return Level.TRACE;
case WARN:
return Level.WARN;
default:
return Level.INFO;
}
}
} }
@@ -0,0 +1,384 @@
/*
* 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.util;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
* become eligible for garbage collection.
*
* <p>If your cached values hold resources that need to be explicitly released,
* override {@link #entryRemoved}.
*
* <p>If a cache miss should be computed on demand for the corresponding keys,
* override {@link #create}. This simplifies the calling code, allowing it to
* assume a value will always be returned, even when there's a cache miss.
*
* <p>By default, the cache size is measured in the number of entries. Override
* {@link #sizeOf} to size the cache in different units. For example, this cache
* is limited to 4MiB of bitmaps:
* <pre> {@code
* int cacheSize = 4 * 1024 * 1024; // 4MiB
* LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
* protected int sizeOf(String key, Bitmap value) {
* return value.getByteCount();
* }
* }}</pre>
*
* <p>This class is thread-safe. Perform multiple cache operations atomically by
* synchronizing on the cache: <pre> {@code
* synchronized (cache) {
* if (cache.get(key) == null) {
* cache.put(key, value);
* }
* }}</pre>
*
* <p>This class does not allow null to be used as a key or value. A return
* value of null from {@link #get}, {@link #put} or {@link #remove} is
* unambiguous: the key was not in the cache.
*
* <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
* of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
* Support Package</a> for earlier releases.
*/
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
/**
* Sets the size of the cache.
*
* @param maxSize The new maximum size.
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
private Map.Entry<K, V> eldest() {
return map.firstEntry();
}
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code key}.
*/
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
*
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false
* if the removal was caused by a {@link #put} or {@link #remove}.
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by
* an eviction or a {@link #remove}.
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
*
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* <p>If a value for {@code key} exists in the cache when this method
* returns, the created value will be released with {@link #entryRemoved}
* and discarded. This can occur when multiple threads request the same key
* at the same time (causing multiple values to be created), or when one
* thread calls {@link #put} while another is creating a value for the same
* key.
*/
protected V create(K key) {
return null;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
* <p>An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
return 1;
}
/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return size;
}
/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
public synchronized final int maxSize() {
return maxSize;
}
/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}
/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
public synchronized final int createCount() {
return createCount;
}
/**
* Returns the number of times {@link #put} was called.
*/
public synchronized final int putCount() {
return putCount;
}
/**
* Returns the number of values that have been evicted.
*/
public synchronized final int evictionCount() {
return evictionCount;
}
/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed.
*/
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(map);
}
@Override public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}
}
@@ -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);
}
}
@@ -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(")");
}
}}
@@ -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();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2014 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.webkit;
import android.net.Uri;
/**
* This class defines a permission request and is used when web content
* requests access to protected resources. The permission request related events
* are delivered via {@link WebChromeClient#onPermissionRequest} and
* {@link WebChromeClient#onPermissionRequestCanceled}.
*
* Either {@link #grant(String[]) grant()} or {@link #deny()} must be called in UI
* thread to respond to the request.
*
* New protected resources whose names are not defined here may be requested in
* future versions of WebView, even when running on an older Android release. To
* avoid unintentionally granting requests for new permissions, you should pass the
* specific permissions you intend to grant to {@link #grant(String[]) grant()},
* and avoid writing code like this example:
* <pre class="prettyprint">
* permissionRequest.grant(permissionRequest.getResources()) // This is wrong!!!
* </pre>
* See the WebView's release notes for information about new protected resources.
*/
public abstract class PermissionRequest {
/**
* Resource belongs to video capture device, like camera.
*/
public final static String RESOURCE_VIDEO_CAPTURE = "android.webkit.resource.VIDEO_CAPTURE";
/**
* Resource belongs to audio capture device, like microphone.
*/
public final static String RESOURCE_AUDIO_CAPTURE = "android.webkit.resource.AUDIO_CAPTURE";
/**
* Resource belongs to protected media identifier.
* After the user grants this resource, the origin can use EME APIs to generate the license
* requests.
*/
public final static String RESOURCE_PROTECTED_MEDIA_ID =
"android.webkit.resource.PROTECTED_MEDIA_ID";
/**
* Resource will allow sysex messages to be sent to or received from MIDI devices. These
* messages are privileged operations, e.g. modifying sound libraries and sampling data, or
* even updating the MIDI device's firmware.
*
* Permission may be requested for this resource in API levels 21 and above, if the Android
* device has been updated to WebView 45 or above.
*/
public final static String RESOURCE_MIDI_SYSEX = "android.webkit.resource.MIDI_SYSEX";
/**
* Call this method to get the origin of the web page which is trying to access
* the restricted resources.
*
* @return the origin of web content which attempt to access the restricted
* resources.
*/
public abstract Uri getOrigin();
/**
* Call this method to get the resources the web page is trying to access.
*
* @return the array of resources the web content wants to access.
*/
public abstract String[] getResources();
/**
* Call this method to grant origin the permission to access the given resources.
* The granted permission is only valid for this WebView.
*
* @param resources the resources granted to be accessed by origin, to grant
* request, the requested resources returned by {@link #getResources()}
* must be equals or a subset of granted resources.
* This parameter is designed to avoid granting permission by accident
* especially when new resources are requested by web content.
*/
public abstract void grant(String[] resources);
/**
* Call this method to deny the request.
*/
public abstract void deny();
}
@@ -0,0 +1,205 @@
/*
* Copyright (C) 2008 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.webkit;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Message;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class WebChromeClient {
public void onProgressChanged(WebView view, int newProgress) {}
public void onReceivedTitle(WebView view, String title) {}
public void onReceivedIcon(WebView view, Bitmap icon) {}
public void onReceivedTouchIconUrl(WebView view, String url,
boolean precomposed) {}
public interface CustomViewCallback {
public void onCustomViewHidden();
}
public void onShowCustomView(View view, CustomViewCallback callback) {};
@Deprecated
public void onShowCustomView(View view, int requestedOrientation,
CustomViewCallback callback) {};
public void onHideCustomView() {}
public boolean onCreateWindow(WebView view, boolean isDialog,
boolean isUserGesture, Message resultMsg) {
return false;
}
public void onRequestFocus(WebView view) {}
public void onCloseWindow(WebView window) {}
public boolean onJsAlert(WebView view, String url, String message,
JsResult result) {
return false;
}
public boolean onJsConfirm(WebView view, String url, String message,
JsResult result) {
return false;
}
public boolean onJsPrompt(WebView view, String url, String message,
String defaultValue, JsPromptResult result) {
return false;
}
public boolean onJsBeforeUnload(WebView view, String url, String message,
JsResult result) {
return false;
}
@Deprecated
public void onExceededDatabaseQuota(String url, String databaseIdentifier,
long quota, long estimatedDatabaseSize, long totalQuota,
WebStorage.QuotaUpdater quotaUpdater) {
// This default implementation passes the current quota back to WebCore.
// WebCore will interpret this that new quota was declined.
quotaUpdater.updateQuota(quota);
}
@Deprecated
public void onReachedMaxAppCacheSize(long requiredStorage, long quota,
WebStorage.QuotaUpdater quotaUpdater) {
quotaUpdater.updateQuota(quota);
}
public void onGeolocationPermissionsShowPrompt(String origin,
GeolocationPermissions.Callback callback) {}
public void onGeolocationPermissionsHidePrompt() {}
public void onPermissionRequest(PermissionRequest request) {
request.deny();
}
public void onPermissionRequestCanceled(PermissionRequest request) {}
// This method was only called when using the JSC javascript engine. V8 became
// the default JS engine with Froyo and support for building with JSC was
// removed in b/5495373. V8 does not have a mechanism for making a callback such
// as this.
@Deprecated
public boolean onJsTimeout() {
return true;
}
@Deprecated
public void onConsoleMessage(String message, int lineNumber, String sourceID) { }
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
// Call the old version of this function for backwards compatability.
onConsoleMessage(consoleMessage.message(), consoleMessage.lineNumber(),
consoleMessage.sourceId());
return false;
}
@Nullable
public Bitmap getDefaultVideoPoster() {
return null;
}
@Nullable
public View getVideoLoadingProgressView() {
return null;
}
/** Obtains a list of all visited history items, used for link coloring
*/
public void getVisitedHistory(ValueCallback<String[]> callback) {
}
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
return false;
}
public static abstract class FileChooserParams {
@SystemApi
public static final long ENABLE_FILE_SYSTEM_ACCESS = 364980165L;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
public @interface Mode {}
/** Open single file. Requires that the file exists before allowing the user to pick it. */
public static final int MODE_OPEN = 0;
/** Like Open but allows multiple files to be selected. */
public static final int MODE_OPEN_MULTIPLE = 1;
/** Like Open but allows a folder to be selected. */
public static final int MODE_OPEN_FOLDER = 2;
/** Allows picking a nonexistent file and saving it. */
public static final int MODE_SAVE = 3;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
public @interface PermissionMode {}
/** File or directory should be opened for reading only. */
public static final int PERMISSION_MODE_READ = 0;
/** File or directory should be opened for read and write. */
public static final int PERMISSION_MODE_READ_WRITE = 1;
@Nullable
public static Uri[] parseResult(int resultCode, Intent data) {
throw new RuntimeException("Stub!");
}
@Mode
public abstract int getMode();
public abstract String[] getAcceptTypes();
public abstract boolean isCaptureEnabled();
@Nullable
public abstract CharSequence getTitle();
@Nullable
public abstract String getFilenameHint();
@PermissionMode
public int getPermissionMode() {
return PERMISSION_MODE_READ;
}
public abstract Intent createIntent();
}
@SystemApi
@Deprecated
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {
uploadFile.onReceiveValue(null);
}
}
@@ -0,0 +1,249 @@
/*
* Copyright (C) 2010 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.webkit;
import android.annotation.SystemApi;
import android.os.Build;
import java.io.InputStream;
import java.io.StringBufferInputStream;
import java.util.Map;
import android.annotation.NonNull;
/**
* Encapsulates a resource response. Applications can return an instance of this
* class from {@link WebViewClient#shouldInterceptRequest} to provide a custom
* response when the WebView requests a particular resource.
*/
public class WebResourceResponse {
private boolean mImmutable;
private String mMimeType;
private String mEncoding;
private int mStatusCode;
private String mReasonPhrase;
private Map<String, String> mResponseHeaders;
private InputStream mInputStream;
/**
* Constructs a resource response with the given MIME type, character encoding,
* and input stream. Callers must implement {@link InputStream#read(byte[])} for
* the input stream. {@link InputStream#close()} will be called after the WebView
* has finished with the response.
*
* <p class="note"><b>Note:</b> The MIME type and character encoding must
* be specified as separate parameters (for example {@code "text/html"} and
* {@code "utf-8"}), not a single value like the {@code "text/html; charset=utf-8"}
* format used in the HTTP Content-Type header. Do not use the value of a HTTP
* Content-Encoding header for {@code encoding}, as that header does not specify a
* character encoding. Content without a defined character encoding (for example
* image resources) should pass {@code null} for {@code encoding}.
*
* @param mimeType the resource response's MIME type, for example {@code "text/html"}.
* @param encoding the resource response's character encoding, for example {@code "utf-8"}.
* @param data the input stream that provides the resource response's data. Must not be a
* StringBufferInputStream.
*/
public WebResourceResponse(String mimeType, String encoding,
InputStream data) {
mMimeType = mimeType;
mEncoding = encoding;
setData(data);
}
/**
* Constructs a resource response with the given parameters. Callers must implement
* {@link InputStream#read(byte[])} for the input stream. {@link InputStream#close()} will be
* called after the WebView has finished with the response.
*
*
* <p class="note"><b>Note:</b> See {@link #WebResourceResponse(String,String,InputStream)}
* for details on what should be specified for {@code mimeType} and {@code encoding}.
*
* @param mimeType the resource response's MIME type, for example {@code "text/html"}.
* @param encoding the resource response's character encoding, for example {@code "utf-8"}.
* @param statusCode the status code needs to be in the ranges [100, 299], [400, 599].
* Causing a redirect by specifying a 3xx code is not supported.
* @param reasonPhrase the phrase describing the status code, for example "OK". Must be
* non-empty.
* @param responseHeaders the resource response's headers represented as a mapping of header
* name -> header value.
* @param data the input stream that provides the resource response's data. Must not be a
* StringBufferInputStream.
*/
public WebResourceResponse(String mimeType, String encoding, int statusCode,
@NonNull String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
this(mimeType, encoding, data);
setStatusCodeAndReasonPhrase(statusCode, reasonPhrase);
setResponseHeaders(responseHeaders);
}
/**
* Sets the resource response's MIME type, for example &quot;text/html&quot;.
*
* @param mimeType The resource response's MIME type
*/
public void setMimeType(String mimeType) {
checkImmutable();
mMimeType = mimeType;
}
/**
* Gets the resource response's MIME type.
*
* @return The resource response's MIME type
*/
public String getMimeType() {
return mMimeType;
}
/**
* Sets the resource response's encoding, for example &quot;UTF-8&quot;. This is used
* to decode the data from the input stream.
*
* @param encoding The resource response's encoding
*/
public void setEncoding(String encoding) {
checkImmutable();
mEncoding = encoding;
}
/**
* Gets the resource response's encoding.
*
* @return The resource response's encoding
*/
public String getEncoding() {
return mEncoding;
}
/**
* Sets the resource response's status code and reason phrase.
*
* @param statusCode the status code needs to be in the ranges [100, 299], [400, 599].
* Causing a redirect by specifying a 3xx code is not supported.
* @param reasonPhrase the phrase describing the status code, for example "OK". Must be
* non-empty.
*/
public void setStatusCodeAndReasonPhrase(int statusCode, @NonNull String reasonPhrase) {
checkImmutable();
if (statusCode < 100)
throw new IllegalArgumentException("statusCode can't be less than 100.");
if (statusCode > 599)
throw new IllegalArgumentException("statusCode can't be greater than 599.");
if (statusCode > 299 && statusCode < 400)
throw new IllegalArgumentException("statusCode can't be in the [300, 399] range.");
if (reasonPhrase == null)
throw new IllegalArgumentException("reasonPhrase can't be null.");
if (reasonPhrase.trim().isEmpty())
throw new IllegalArgumentException("reasonPhrase can't be empty.");
for (int i = 0; i < reasonPhrase.length(); i++) {
int c = reasonPhrase.charAt(i);
if (c > 0x7F) {
throw new IllegalArgumentException(
"reasonPhrase can't contain non-ASCII characters.");
}
}
mStatusCode = statusCode;
mReasonPhrase = reasonPhrase;
}
/**
* Gets the resource response's status code.
*
* @return The resource response's status code.
*/
public int getStatusCode() {
return mStatusCode;
}
/**
* Gets the description of the resource response's status code.
*
* @return The description of the resource response's status code.
*/
public String getReasonPhrase() {
return mReasonPhrase;
}
/**
* Sets the headers for the resource response.
*
* @param headers Mapping of header name -> header value.
*/
public void setResponseHeaders(Map<String, String> headers) {
checkImmutable();
mResponseHeaders = headers;
}
/**
* Gets the headers for the resource response.
*
* @return The headers for the resource response.
*/
public Map<String, String> getResponseHeaders() {
return mResponseHeaders;
}
/**
* Sets the input stream that provides the resource response's data. Callers
* must implement {@link InputStream#read(byte[])}. {@link InputStream#close()}
* will be called after the WebView has finished with the response.
*
* @param data the input stream that provides the resource response's data. Must not be a
* StringBufferInputStream.
*/
public void setData(InputStream data) {
checkImmutable();
// If data is (or is a subclass of) StringBufferInputStream
if (data != null && StringBufferInputStream.class.isAssignableFrom(data.getClass())) {
throw new IllegalArgumentException("StringBufferInputStream is deprecated and must " +
"not be passed to a WebResourceResponse");
}
mInputStream = data;
}
/**
* Gets the input stream that provides the resource response's data.
*
* @return The input stream that provides the resource response's data
*/
public InputStream getData() {
return mInputStream;
}
/**
* The internal version of the constructor that doesn't perform arguments checks.
* @hide
*/
@SystemApi
public WebResourceResponse(boolean immutable, String mimeType, String encoding, int statusCode,
String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
mImmutable = immutable;
mMimeType = mimeType;
mEncoding = encoding;
mStatusCode = statusCode;
mReasonPhrase = reasonPhrase;
mResponseHeaders = responseHeaders;
mInputStream = data;
}
private void checkImmutable() {
if (mImmutable)
throw new IllegalStateException("This WebResourceResponse instance is immutable");
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,609 @@
/*
* Copyright (C) 2008 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.webkit;
import android.annotation.Nullable;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Message;
import android.view.InputEvent;
import android.view.KeyEvent;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class WebViewClient {
/**
* Give the host application a chance to take control when a URL is about to be loaded in the
* current WebView. If a WebViewClient is not provided, by default WebView will ask Activity
* Manager to choose the proper handler for the URL. If a WebViewClient is provided, returning
* {@code true} causes the current WebView to abort loading the URL, while returning
* {@code false} causes the WebView to continue loading the URL as usual.
*
* <p class="note"><b>Note:</b> Do not call {@link WebView#loadUrl(String)} with the same
* URL and then return {@code true}. This unnecessarily cancels the current load and starts a
* new load with the same URL. The correct way to continue loading a given URL is to simply
* return {@code false}, without calling {@link WebView#loadUrl(String)}.
*
* <p class="note"><b>Note:</b> This method is not called for POST requests.
*
* <p class="note"><b>Note:</b> This method may be called for subframes and with non-HTTP(S)
* schemes; calling {@link WebView#loadUrl(String)} with such a URL will fail.
*
* @param view The WebView that is initiating the callback.
* @param url The URL to be loaded.
* @return {@code true} to cancel the current load, otherwise return {@code false}.
* @deprecated Use {@link #shouldOverrideUrlLoading(WebView, WebResourceRequest)
* shouldOverrideUrlLoading(WebView, WebResourceRequest)} instead.
*/
@Deprecated
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
/**
* Give the host application a chance to take control when a URL is about to be loaded in the
* current WebView. If a WebViewClient is not provided, by default WebView will ask Activity
* Manager to choose the proper handler for the URL. If a WebViewClient is provided, returning
* {@code true} causes the current WebView to abort loading the URL, while returning
* {@code false} causes the WebView to continue loading the URL as usual.
*
* <p>This callback is not called for all page navigations. In particular, this is not called
* for navigations which the app initiated with {@code loadUrl()}: this callback would not serve
* a purpose in this case, because the app already knows about the navigation. This callback
* lets the app know about navigations initiated by the web page (such as navigations initiated
* by JavaScript code), by the user (such as when the user taps on a link), or by an HTTP
* redirect (ex. if {@code loadUrl("foo.com")} redirects to {@code "bar.com"} because of HTTP
* 301).
*
* <p class="note"><b>Note:</b> Do not call {@link WebView#loadUrl(String)} with the request's
* URL and then return {@code true}. This unnecessarily cancels the current load and starts a
* new load with the same URL. The correct way to continue loading a given URL is to simply
* return {@code false}, without calling {@link WebView#loadUrl(String)}.
*
* <p class="note"><b>Note:</b> This method is not called for POST requests.
*
* <p class="note"><b>Note:</b> This method may be called for subframes and with non-HTTP(S)
* schemes; calling {@link WebView#loadUrl(String)} with such a URL will fail.
*
* @param view The WebView that is initiating the callback.
* @param request Object containing the details of the request.
* @return {@code true} to cancel the current load, otherwise return {@code false}.
*/
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return shouldOverrideUrlLoading(view, request.getUrl().toString());
}
/**
* Notify the host application that a page has started loading. This method
* is called once for each main frame load so a page with iframes or
* framesets will call onPageStarted one time for the main frame. This also
* means that onPageStarted will not be called when the contents of an
* embedded frame changes, i.e. clicking a link whose target is an iframe,
* it will also not be called for fragment navigations (navigations to
* #fragment_id).
*
* @param view The WebView that is initiating the callback.
* @param url The url to be loaded.
* @param favicon The favicon for this page if it already exists in the
* database.
*/
public void onPageStarted(WebView view, String url, Bitmap favicon) {
}
/**
* Notify the host application that a page has finished loading. This method
* is called only for main frame. Receiving an {@code onPageFinished()} callback does not
* guarantee that the next frame drawn by WebView will reflect the state of the DOM at this
* point. In order to be notified that the current DOM state is ready to be rendered, request a
* visual state callback with {@link WebView#postVisualStateCallback} and wait for the supplied
* callback to be triggered.
*
* @param view The WebView that is initiating the callback.
* @param url The url of the page.
*/
public void onPageFinished(WebView view, String url) {
}
/**
* Notify the host application that the WebView will load the resource
* specified by the given url.
*
* @param view The WebView that is initiating the callback.
* @param url The url of the resource the WebView will load.
*/
public void onLoadResource(WebView view, String url) {
}
/**
* Notify the host application that {@link android.webkit.WebView} content left over from
* previous page navigations will no longer be drawn.
*
* <p>This callback can be used to determine the point at which it is safe to make a recycled
* {@link android.webkit.WebView} visible, ensuring that no stale content is shown. It is called
* at the earliest point at which it can be guaranteed that {@link WebView#onDraw} will no
* longer draw any content from previous navigations. The next draw will display either the
* {@link WebView#setBackgroundColor background color} of the {@link WebView}, or some of the
* contents of the newly loaded page.
*
* <p>This method is called when the body of the HTTP response has started loading, is reflected
* in the DOM, and will be visible in subsequent draws. This callback occurs early in the
* document loading process, and as such you should expect that linked resources (for example,
* CSS and images) may not be available.
*
* <p>For more fine-grained notification of visual state updates, see {@link
* WebView#postVisualStateCallback}.
*
* <p>Please note that all the conditions and recommendations applicable to
* {@link WebView#postVisualStateCallback} also apply to this API.
*
* <p>This callback is only called for main frame navigations.
*
* @param view The {@link android.webkit.WebView} for which the navigation occurred.
* @param url The URL corresponding to the page navigation that triggered this callback.
*/
public void onPageCommitVisible(WebView view, String url) {
}
/**
* Notify the host application of a resource request and allow the
* application to return the data. If the return value is {@code null}, the WebView
* will continue to load the resource as usual. Otherwise, the return
* response and data will be used.
*
* <p>This callback is invoked for a variety of URL schemes (e.g., {@code http(s):}, {@code
* data:}, {@code file:}, etc.), not only those schemes which send requests over the network.
* This is not called for {@code javascript:} URLs, {@code blob:} URLs, or for assets accessed
* via {@code file:///android_asset/} or {@code file:///android_res/} URLs.
*
* <p>In the case of redirects, this is only called for the initial resource URL, not any
* subsequent redirect URLs.
*
* <p class="note"><b>Note:</b> This method is called on a thread
* other than the UI thread so clients should exercise caution
* when accessing private data or the view system.
*
* <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
* Browsing checks. If this is undesired, you can use {@link WebView#setSafeBrowsingWhitelist}
* to skip Safe Browsing checks for that host or dismiss the warning in {@link
* #onSafeBrowsingHit} by calling {@link SafeBrowsingResponse#proceed}.
*
* @param view The {@link android.webkit.WebView} that is requesting the
* resource.
* @param url The raw url of the resource.
* @return A {@link android.webkit.WebResourceResponse} containing the
* response information or {@code null} if the WebView should load the
* resource itself.
* @deprecated Use {@link #shouldInterceptRequest(WebView, WebResourceRequest)
* shouldInterceptRequest(WebView, WebResourceRequest)} instead.
*/
@Deprecated
@Nullable
public WebResourceResponse shouldInterceptRequest(WebView view,
String url) {
return null;
}
/**
* Notify the host application of a resource request and allow the
* application to return the data. If the return value is {@code null}, the WebView
* will continue to load the resource as usual. Otherwise, the return
* response and data will be used.
*
* <p>This callback is invoked for a variety of URL schemes (e.g., {@code http(s):}, {@code
* data:}, {@code file:}, etc.), not only those schemes which send requests over the network.
* This is not called for {@code javascript:} URLs, {@code blob:} URLs, or for assets accessed
* via {@code file:///android_asset/} or {@code file:///android_res/} URLs.
*
* <p>In the case of redirects, this is only called for the initial resource URL, not any
* subsequent redirect URLs.
*
* <p class="note"><b>Note:</b> This method is called on a thread
* other than the UI thread so clients should exercise caution
* when accessing private data or the view system.
*
* <p class="note"><b>Note:</b> When Safe Browsing is enabled, these URLs still undergo Safe
* Browsing checks. If this is undesired, you can use {@link WebView#setSafeBrowsingWhitelist}
* to skip Safe Browsing checks for that host or dismiss the warning in {@link
* #onSafeBrowsingHit} by calling {@link SafeBrowsingResponse#proceed}.
*
* @param view The {@link android.webkit.WebView} that is requesting the
* resource.
* @param request Object containing the details of the request.
* @return A {@link android.webkit.WebResourceResponse} containing the
* response information or {@code null} if the WebView should load the
* resource itself.
*/
@Nullable
public WebResourceResponse shouldInterceptRequest(WebView view,
WebResourceRequest request) {
return shouldInterceptRequest(view, request.getUrl().toString());
}
/**
* Notify the host application that there have been an excessive number of
* HTTP redirects. As the host application if it would like to continue
* trying to load the resource. The default behavior is to send the cancel
* message.
*
* @param view The WebView that is initiating the callback.
* @param cancelMsg The message to send if the host wants to cancel
* @param continueMsg The message to send if the host wants to continue
* @deprecated This method is no longer called. When the WebView encounters
* a redirect loop, it will cancel the load.
*/
@Deprecated
public void onTooManyRedirects(WebView view, Message cancelMsg,
Message continueMsg) {
cancelMsg.sendToTarget();
}
// These ints must match up to the hidden values in EventHandler.
/** Generic error */
public static final int ERROR_UNKNOWN = -1;
/** Server or proxy hostname lookup failed */
public static final int ERROR_HOST_LOOKUP = -2;
/** Unsupported authentication scheme (not basic or digest) */
public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3;
/** User authentication failed on server */
public static final int ERROR_AUTHENTICATION = -4;
/** User authentication failed on proxy */
public static final int ERROR_PROXY_AUTHENTICATION = -5;
/** Failed to connect to the server */
public static final int ERROR_CONNECT = -6;
/** Failed to read or write to the server */
public static final int ERROR_IO = -7;
/** Connection timed out */
public static final int ERROR_TIMEOUT = -8;
/** Too many redirects */
public static final int ERROR_REDIRECT_LOOP = -9;
/** Unsupported URI scheme */
public static final int ERROR_UNSUPPORTED_SCHEME = -10;
/** Failed to perform SSL handshake */
public static final int ERROR_FAILED_SSL_HANDSHAKE = -11;
/** Malformed URL */
public static final int ERROR_BAD_URL = -12;
/** Generic file error */
public static final int ERROR_FILE = -13;
/** File not found */
public static final int ERROR_FILE_NOT_FOUND = -14;
/** Too many requests during this load */
public static final int ERROR_TOO_MANY_REQUESTS = -15;
/** Resource load was canceled by Safe Browsing */
public static final int ERROR_UNSAFE_RESOURCE = -16;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
public @interface SafeBrowsingThreat {}
/** The resource was blocked for an unknown reason. */
public static final int SAFE_BROWSING_THREAT_UNKNOWN = 0;
/** The resource was blocked because it contains malware. */
public static final int SAFE_BROWSING_THREAT_MALWARE = 1;
/** The resource was blocked because it contains deceptive content. */
public static final int SAFE_BROWSING_THREAT_PHISHING = 2;
/** The resource was blocked because it contains unwanted software. */
public static final int SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE = 3;
/**
* The resource was blocked because it may trick the user into a billing agreement.
*
* <p>This constant is only used when targetSdkVersion is at least {@link
* android.os.Build.VERSION_CODES#Q}. Otherwise, {@link #SAFE_BROWSING_THREAT_UNKNOWN} is used
* instead.
*/
public static final int SAFE_BROWSING_THREAT_BILLING = 4;
/**
* Report an error to the host application. These errors are unrecoverable
* (i.e. the main resource is unavailable). The {@code errorCode} parameter
* corresponds to one of the {@code ERROR_*} constants.
* @param view The WebView that is initiating the callback.
* @param errorCode The error code corresponding to an ERROR_* value.
* @param description A String describing the error.
* @param failingUrl The url that failed to load.
* @deprecated Use {@link #onReceivedError(WebView, WebResourceRequest, WebResourceError)
* onReceivedError(WebView, WebResourceRequest, WebResourceError)} instead.
*/
@Deprecated
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
}
/**
* Report web resource loading error to the host application. These errors usually indicate
* inability to connect to the server. Note that unlike the deprecated version of the callback,
* the new version will be called for any resource (iframe, image, etc.), not just for the main
* page. Thus, it is recommended to perform minimum required work in this callback.
* @param view The WebView that is initiating the callback.
* @param request The originating request.
* @param error Information about the error occurred.
*/
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
if (request.isForMainFrame()) {
onReceivedError(view,
error.getErrorCode(), error.getDescription().toString(),
request.getUrl().toString());
}
}
/**
* Notify the host application that an HTTP error has been received from the server while
* loading a resource. HTTP errors have status codes &gt;= 400. This callback will be called
* for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended
* to perform minimum required work in this callback. Note that the content of the server
* response may not be provided within the {@code errorResponse} parameter.
* @param view The WebView that is initiating the callback.
* @param request The originating request.
* @param errorResponse Information about the error occurred.
*/
public void onReceivedHttpError(
WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
}
/**
* As the host application if the browser should resend data as the
* requested page was a result of a POST. The default is to not resend the
* data.
*
* @param view The WebView that is initiating the callback.
* @param dontResend The message to send if the browser should not resend
* @param resend The message to send if the browser should resend data
*/
public void onFormResubmission(WebView view, Message dontResend,
Message resend) {
dontResend.sendToTarget();
}
/**
* Notify the host application to update its visited links database.
*
* @param view The WebView that is initiating the callback.
* @param url The url being visited.
* @param isReload {@code true} if this url is being reloaded.
*/
public void doUpdateVisitedHistory(WebView view, String url,
boolean isReload) {
}
/**
* Notifies the host application that an SSL error occurred while loading a
* resource. The host application must call either
* {@link SslErrorHandler#cancel()} or {@link SslErrorHandler#proceed()}.
*
* <p class="warning"><b>Warning:</b> Application overrides of this method
* can be used to display custom error pages or to silently log issues, but
* the host application should always call {@code SslErrorHandler#cancel()}
* and never proceed past errors.
*
* <p class="note"><b>Note:</b> Do not prompt the user about SSL errors.
* Users are unlikely to be able to make an informed security decision, and
* {@code WebView} does not provide a UI for showing the details of the
* error in a meaningful way.
*
* <p>The decision to call {@code proceed()} or {@code cancel()} may be
* retained to facilitate responses to future SSL errors. The default
* behavior is to cancel the resource loading process.
*
* <p>This API is called only for recoverable SSL certificate errors. For
* non-recoverable errors (such as when the server fails the client), the
* {@code WebView} calls {@link #onReceivedError(WebView,
* WebResourceRequest, WebResourceError) onReceivedError(WebView,
* WebResourceRequest, WebResourceError)} with the
* {@link #ERROR_FAILED_SSL_HANDSHAKE} argument.
*
* @param view {@code WebView} that initiated the callback.
* @param handler {@link SslErrorHandler} that handles the user's response.
* @param error SSL error object.
*/
public void onReceivedSslError(WebView view, SslErrorHandler handler,
SslError error) {
handler.cancel();
}
/**
* Notify the host application to handle a SSL client certificate request. The host application
* is responsible for showing the UI if desired and providing the keys. There are three ways to
* respond: {@link ClientCertRequest#proceed}, {@link ClientCertRequest#cancel}, or {@link
* ClientCertRequest#ignore}. Webview stores the response in memory (for the life of the
* application) if {@link ClientCertRequest#proceed} or {@link ClientCertRequest#cancel} is
* called and does not call {@code onReceivedClientCertRequest()} again for the same host and
* port pair. Webview does not store the response if {@link ClientCertRequest#ignore}
* is called. Note that, multiple layers in chromium network stack might be
* caching the responses, so the behavior for ignore is only a best case
* effort.
*
* This method is called on the UI thread. During the callback, the
* connection is suspended.
*
* For most use cases, the application program should implement the
* {@link android.security.KeyChainAliasCallback} interface and pass it to
* {@link android.security.KeyChain#choosePrivateKeyAlias} to start an
* activity for the user to choose the proper alias. The keychain activity will
* provide the alias through the callback method in the implemented interface. Next
* the application should create an async task to call
* {@link android.security.KeyChain#getPrivateKey} to receive the key.
*
* An example implementation of client certificates can be seen at
* <A href="https://android.googlesource.com/platform/packages/apps/Browser/+/android-5.1.1_r1/src/com/android/browser/Tab.java">
* AOSP Browser</a>
*
* The default behavior is to cancel, returning no client certificate.
*
* @param view The WebView that is initiating the callback
* @param request An instance of a {@link ClientCertRequest}
*
*/
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
request.cancel();
}
/**
* Notifies the host application that the WebView received an HTTP
* authentication request. The host application can use the supplied
* {@link HttpAuthHandler} to set the WebView's response to the request.
* The default behavior is to cancel the request.
*
* <p class="note"><b>Note:</b> The supplied HttpAuthHandler must be used on
* the UI thread.
*
* @param view the WebView that is initiating the callback
* @param handler the HttpAuthHandler used to set the WebView's response
* @param host the host requiring authentication
* @param realm the realm for which authentication is required
* @see WebView#getHttpAuthUsernamePassword
*/
public void onReceivedHttpAuthRequest(WebView view,
HttpAuthHandler handler, String host, String realm) {
handler.cancel();
}
/**
* Give the host application a chance to handle the key event synchronously.
* e.g. menu shortcut key events need to be filtered this way. If return
* true, WebView will not handle the key event. If return {@code false}, WebView
* will always handle the key event, so none of the super in the view chain
* will see the key event. The default behavior returns {@code false}.
*
* @param view The WebView that is initiating the callback.
* @param event The key event.
* @return {@code true} if the host application wants to handle the key event
* itself, otherwise return {@code false}
*/
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
return false;
}
/**
* Notify the host application that a key was not handled by the WebView.
* Except system keys, WebView always consumes the keys in the normal flow
* or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
* from where the key is dispatched. It gives the host application a chance
* to handle the unhandled key events.
*
* @param view The WebView that is initiating the callback.
* @param event The key event.
*/
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
onUnhandledInputEventInternal(view, event);
}
/**
* Notify the host application that a input event was not handled by the WebView.
* Except system keys, WebView always consumes input events in the normal flow
* or if {@link #shouldOverrideKeyEvent} returns {@code true}. This is called asynchronously
* from where the event is dispatched. It gives the host application a chance
* to handle the unhandled input events.
*
* Note that if the event is a {@link android.view.MotionEvent}, then it's lifetime is only
* that of the function call. If the WebViewClient wishes to use the event beyond that, then it
* <i>must</i> create a copy of the event.
*
* It is the responsibility of overriders of this method to call
* {@link #onUnhandledKeyEvent(WebView, KeyEvent)}
* when appropriate if they wish to continue receiving events through it.
*
* @param view The WebView that is initiating the callback.
* @param event The input event.
* @removed
*/
public void onUnhandledInputEvent(WebView view, InputEvent event) {
if (event instanceof KeyEvent) {
onUnhandledKeyEvent(view, (KeyEvent) event);
return;
}
onUnhandledInputEventInternal(view, event);
}
private void onUnhandledInputEventInternal(WebView view, InputEvent event) {
throw new RuntimeException("Stub!");
}
/**
* Notify the host application that the scale applied to the WebView has
* changed.
*
* @param view The WebView that is initiating the callback.
* @param oldScale The old scale factor
* @param newScale The new scale factor
*/
public void onScaleChanged(WebView view, float oldScale, float newScale) {
}
/**
* Notify the host application that a request to automatically log in the
* user has been processed.
* @param view The WebView requesting the login.
* @param realm The account realm used to look up accounts.
* @param account An optional account. If not {@code null}, the account should be
* checked against accounts on the device. If it is a valid
* account, it should be used to log in the user.
* @param args Authenticator specific arguments used to log in the user.
*/
public void onReceivedLoginRequest(WebView view, String realm,
@Nullable String account, String args) {
}
/**
* Notify host application that the given WebView's render process has exited.
*
* Multiple WebView instances may be associated with a single render process;
* onRenderProcessGone will be called for each WebView that was affected.
* The application's implementation of this callback should only attempt to
* clean up the specific WebView given as a parameter, and should not assume
* that other WebView instances are affected.
*
* The given WebView can't be used, and should be removed from the view hierarchy,
* all references to it should be cleaned up, e.g any references in the Activity
* or other classes saved using {@link android.view.View#findViewById} and similar calls, etc.
*
* To cause an render process crash for test purpose, the application can
* call {@code loadUrl("chrome://crash")} on the WebView. Note that multiple WebView
* instances may be affected if they share a render process, not just the
* specific WebView which loaded chrome://crash.
*
* @param view The WebView which needs to be cleaned up.
* @param detail the reason why it exited.
* @return {@code true} if the host application handled the situation that process has
* exited, otherwise, application will crash if render process crashed,
* or be killed if render process was killed by the system.
*/
public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
return false;
}
/**
* Notify the host application that a loading URL has been flagged by Safe Browsing.
*
* The application must invoke the callback to indicate the preferred response. The default
* behavior is to show an interstitial to the user, with the reporting checkbox visible.
*
* If the application needs to show its own custom interstitial UI, the callback can be invoked
* asynchronously with {@link SafeBrowsingResponse#backToSafety} or {@link
* SafeBrowsingResponse#proceed}, depending on user response.
*
* @param view The WebView that hit the malicious resource.
* @param request Object containing the details of the request.
* @param threatType The reason the resource was caught by Safe Browsing, corresponding to a
* {@code SAFE_BROWSING_THREAT_*} value.
* @param callback Applications must invoke one of the callback methods.
*/
public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
@SafeBrowsingThreat int threatType, SafeBrowsingResponse callback) {
callback.showInterstitial(/* allowReporting */ true);
}
}
@@ -0,0 +1,541 @@
/*
* Copyright (C) 2012 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.webkit;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.http.SslCertificate;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.print.PrintDocumentAdapter;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.view.DragEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.textclassifier.TextClassifier;
import android.webkit.WebView.HitTestResult;
import android.webkit.WebView.PictureListener;
import android.webkit.WebView.VisualStateCallback;
import java.io.BufferedWriter;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* WebView backend provider interface: this interface is the abstract backend to a WebView
* instance; each WebView object is bound to exactly one WebViewProvider object which implements
* the runtime behavior of that WebView.
*
* All methods must behave as per their namesake in {@link WebView}, unless otherwise noted.
*
* @hide Not part of the public API; only required by system implementors.
*/
@SystemApi
public interface WebViewProvider {
//-------------------------------------------------------------------------
// Main interface for backend provider of the WebView class.
//-------------------------------------------------------------------------
/**
* Initialize this WebViewProvider instance. Called after the WebView has fully constructed.
* @param javaScriptInterfaces is a Map of interface names, as keys, and
* object implementing those interfaces, as values.
* @param privateBrowsing If {@code true} the web view will be initialized in private /
* incognito mode.
*/
public void init(Map<String, Object> javaScriptInterfaces,
boolean privateBrowsing);
// Deprecated - should never be called
public void setHorizontalScrollbarOverlay(boolean overlay);
// Deprecated - should never be called
public void setVerticalScrollbarOverlay(boolean overlay);
// Deprecated - should never be called
public boolean overlayHorizontalScrollbar();
// Deprecated - should never be called
public boolean overlayVerticalScrollbar();
public int getVisibleTitleHeight();
public SslCertificate getCertificate();
public void setCertificate(SslCertificate certificate);
public void savePassword(String host, String username, String password);
public void setHttpAuthUsernamePassword(String host, String realm,
String username, String password);
public String[] getHttpAuthUsernamePassword(String host, String realm);
/**
* See {@link WebView#destroy()}.
* As well as releasing the internal state and resources held by the implementation,
* the provider should null all references it holds on the WebView proxy class, and ensure
* no further method calls are made to it.
*/
public void destroy();
public void setNetworkAvailable(boolean networkUp);
public WebBackForwardList saveState(Bundle outState);
public boolean savePicture(Bundle b, final File dest);
public boolean restorePicture(Bundle b, File src);
public WebBackForwardList restoreState(Bundle inState);
public void loadUrl(String url, Map<String, String> additionalHttpHeaders);
public void loadUrl(String url);
public void postUrl(String url, byte[] postData);
public void loadData(String data, String mimeType, String encoding);
public void loadDataWithBaseURL(String baseUrl, String data,
String mimeType, String encoding, String historyUrl);
public void evaluateJavaScript(String script, ValueCallback<String> resultCallback);
public void saveWebArchive(String filename);
public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback);
public void stopLoading();
public void reload();
public boolean canGoBack();
public void goBack();
public boolean canGoForward();
public void goForward();
public boolean canGoBackOrForward(int steps);
public void goBackOrForward(int steps);
public boolean isPrivateBrowsingEnabled();
public boolean pageUp(boolean top);
public boolean pageDown(boolean bottom);
public void insertVisualStateCallback(long requestId, VisualStateCallback callback);
public void clearView();
public Picture capturePicture();
public PrintDocumentAdapter createPrintDocumentAdapter(String documentName);
public float getScale();
public void setInitialScale(int scaleInPercent);
public void invokeZoomPicker();
public HitTestResult getHitTestResult();
public void requestFocusNodeHref(Message hrefMsg);
public void requestImageRef(Message msg);
public String getUrl();
public String getOriginalUrl();
public String getTitle();
public Bitmap getFavicon();
public String getTouchIconUrl();
public int getProgress();
public int getContentHeight();
public int getContentWidth();
public void pauseTimers();
public void resumeTimers();
public void onPause();
public void onResume();
public boolean isPaused();
public void freeMemory();
public void clearCache(boolean includeDiskFiles);
public void clearFormData();
public void clearHistory();
public void clearSslPreferences();
public WebBackForwardList copyBackForwardList();
public void setFindListener(WebView.FindListener listener);
public void findNext(boolean forward);
public int findAll(String find);
public void findAllAsync(String find);
public boolean showFindDialog(String text, boolean showIme);
public void clearMatches();
public void documentHasImages(Message response);
public void setWebViewClient(WebViewClient client);
public WebViewClient getWebViewClient();
@Nullable
public WebViewRenderProcess getWebViewRenderProcess();
public void setWebViewRenderProcessClient(
@Nullable Executor executor,
@Nullable WebViewRenderProcessClient client);
@Nullable
public WebViewRenderProcessClient getWebViewRenderProcessClient();
public void setDownloadListener(DownloadListener listener);
public void setWebChromeClient(WebChromeClient client);
public WebChromeClient getWebChromeClient();
public void setPictureListener(PictureListener listener);
public void addJavascriptInterface(Object obj, String interfaceName);
public void removeJavascriptInterface(String interfaceName);
public WebMessagePort[] createWebMessageChannel();
public void postMessageToMainFrame(WebMessage message, Uri targetOrigin);
public WebSettings getSettings();
public void setMapTrackballToArrowKeys(boolean setMap);
public void flingScroll(int vx, int vy);
public View getZoomControls();
public boolean canZoomIn();
public boolean canZoomOut();
public boolean zoomBy(float zoomFactor);
public boolean zoomIn();
public boolean zoomOut();
public void dumpViewHierarchyWithProperties(BufferedWriter out, int level);
public View findHierarchyView(String className, int hashCode);
public void setRendererPriorityPolicy(int rendererRequestedPriority, boolean waivedWhenNotVisible);
public int getRendererRequestedPriority();
public boolean getRendererPriorityWaivedWhenNotVisible();
@SuppressWarnings("unused")
public default void setTextClassifier(@Nullable TextClassifier textClassifier) {}
@NonNull
public default TextClassifier getTextClassifier() { return TextClassifier.NO_OP; }
//-------------------------------------------------------------------------
// Provider internal methods
//-------------------------------------------------------------------------
/**
* @return the ViewDelegate implementation. This provides the functionality to back all of
* the name-sake functions from the View and ViewGroup base classes of WebView.
*/
/* package */ ViewDelegate getViewDelegate();
/**
* @return a ScrollDelegate implementation. Normally this would be same object as is
* returned by getViewDelegate().
*/
/* package */ ScrollDelegate getScrollDelegate();
/**
* Only used by FindActionModeCallback to inform providers that the find dialog has
* been dismissed.
*/
public void notifyFindDialogDismissed();
//-------------------------------------------------------------------------
// View / ViewGroup delegation methods
//-------------------------------------------------------------------------
/**
* Provides mechanism for the name-sake methods declared in View and ViewGroup to be delegated
* into the WebViewProvider instance.
* NOTE: For many of these methods, the WebView will provide a super.Foo() call before or after
* making the call into the provider instance. This is done for convenience in the common case
* of maintaining backward compatibility. For remaining super class calls (e.g. where the
* provider may need to only conditionally make the call based on some internal state) see the
* {@link WebView.PrivateAccess} callback class.
*/
// TODO: See if the pattern of the super-class calls can be rationalized at all, and document
// the remainder on the methods below.
interface ViewDelegate {
public boolean shouldDelayChildPressedState();
public void onProvideVirtualStructure(android.view.ViewStructure structure);
default void onProvideAutofillVirtualStructure(
@SuppressWarnings("unused") android.view.ViewStructure structure,
@SuppressWarnings("unused") int flags) {
}
default void autofill(@SuppressWarnings("unused") SparseArray<AutofillValue> values) {
}
default boolean isVisibleToUserForAutofill(@SuppressWarnings("unused") int virtualId) {
return true; // true is the default value returned by View.isVisibleToUserForAutofill()
}
default void onProvideContentCaptureStructure(
@NonNull @SuppressWarnings("unused") android.view.ViewStructure structure,
@SuppressWarnings("unused") int flags) {
}
// @SuppressLint("NullableCollection")
// default void onCreateVirtualViewTranslationRequests(
// @NonNull @SuppressWarnings("unused") long[] virtualIds,
// @NonNull @SuppressWarnings("unused") @DataFormat int[] supportedFormats,
// @NonNull @SuppressWarnings("unused")
// Consumer<ViewTranslationRequest> requestsCollector) {
// }
// default void onVirtualViewTranslationResponses(
// @NonNull @SuppressWarnings("unused")
// LongSparseArray<ViewTranslationResponse> response) {
// }
// default void dispatchCreateViewTranslationRequest(
// @NonNull @SuppressWarnings("unused") Map<AutofillId, long[]> viewIds,
// @NonNull @SuppressWarnings("unused") @DataFormat int[] supportedFormats,
// @Nullable @SuppressWarnings("unused") TranslationCapability capability,
// @NonNull @SuppressWarnings("unused") List<ViewTranslationRequest> requests) {
// }
public AccessibilityNodeProvider getAccessibilityNodeProvider();
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info);
public void onInitializeAccessibilityEvent(AccessibilityEvent event);
public boolean performAccessibilityAction(int action, Bundle arguments);
public void setOverScrollMode(int mode);
public void setScrollBarStyle(int style);
public void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t,
int r, int b);
public void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY);
public void onWindowVisibilityChanged(int visibility);
public void onDraw(Canvas canvas);
public void setLayoutParams(LayoutParams layoutParams);
public boolean performLongClick();
public void onConfigurationChanged(Configuration newConfig);
public InputConnection onCreateInputConnection(EditorInfo outAttrs);
public boolean onDragEvent(DragEvent event);
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event);
public boolean onKeyDown(int keyCode, KeyEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onAttachedToWindow();
public void onDetachedFromWindow();
public default void onMovedToDisplay(int displayId, Configuration config) {}
public void onVisibilityChanged(View changedView, int visibility);
public void onWindowFocusChanged(boolean hasWindowFocus);
public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect);
public boolean setFrame(int left, int top, int right, int bottom);
public void onSizeChanged(int w, int h, int ow, int oh);
public void onScrollChanged(int l, int t, int oldl, int oldt);
public boolean dispatchKeyEvent(KeyEvent event);
public boolean onTouchEvent(MotionEvent ev);
public boolean onHoverEvent(MotionEvent event);
public boolean onGenericMotionEvent(MotionEvent event);
public boolean onTrackballEvent(MotionEvent ev);
public boolean requestFocus(int direction, Rect previouslyFocusedRect);
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate);
public void setBackgroundColor(int color);
public void setLayerType(int layerType, Paint paint);
public void preDispatchDraw(Canvas canvas);
public void onStartTemporaryDetach();
public void onFinishTemporaryDetach();
public void onActivityResult(int requestCode, int resultCode, Intent data);
public Handler getHandler(Handler originalHandler);
public View findFocus(View originalFocusedView);
@SuppressWarnings("unused")
default boolean onCheckIsTextEditor() {
return false;
}
/**
* @see View#onApplyWindowInsets(WindowInsets).
*
* <p>This is the entry point for the WebView implementation to override. It returns
* {@code null} when the WebView implementation hasn't implemented the WindowInsets support
* on S yet. In this case, the {@link View#onApplyWindowInsets()} super method will be
* called instead.
*
* @param insets Insets to apply
* @return The supplied insets with any applied insets consumed.
*/
@SuppressWarnings("unused")
@Nullable
default WindowInsets onApplyWindowInsets(@Nullable WindowInsets insets) {
return null;
}
/**
* @hide Only used by WebView.
*/
@SuppressWarnings("unused")
@Nullable
default PointerIcon onResolvePointerIcon(@NonNull MotionEvent event, int pointerIndex) {
return null;
}
}
interface ScrollDelegate {
// These methods are declared protected in the ViewGroup base class. This interface
// exists to promote them to public so they may be called by the WebView proxy class.
// TODO: Combine into ViewDelegate?
/**
* See {@link android.webkit.WebView#computeHorizontalScrollRange}
*/
public int computeHorizontalScrollRange();
/**
* See {@link android.webkit.WebView#computeHorizontalScrollOffset}
*/
public int computeHorizontalScrollOffset();
/**
* See {@link android.webkit.WebView#computeVerticalScrollRange}
*/
public int computeVerticalScrollRange();
/**
* See {@link android.webkit.WebView#computeVerticalScrollOffset}
*/
public int computeVerticalScrollOffset();
/**
* See {@link android.webkit.WebView#computeVerticalScrollExtent}
*/
public int computeVerticalScrollExtent();
/**
* See {@link android.webkit.WebView#computeScroll}
*/
public void computeScroll();
}
}
@@ -0,0 +1,108 @@
/*
* 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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews.RemoteView;
@Deprecated
@RemoteView
public class AbsoluteLayout extends ViewGroup {
public AbsoluteLayout(Context context) {
this(context, null);
}
public AbsoluteLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
throw new RuntimeException("Stub!");
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0);
}
@Override
protected void onLayout(boolean changed, int l, int t,
int r, int b) {
throw new RuntimeException("Stub!");
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new AbsoluteLayout.LayoutParams(getContext(), attrs);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof AbsoluteLayout.LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
public static class LayoutParams extends ViewGroup.LayoutParams {
public int x;
public int y;
public LayoutParams(int width, int height, int x, int y) {
super(width, height);
this.x = x;
this.y = y;
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
throw new RuntimeException("Stub!");
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public String debug(String output) {
return output + "Absolute.LayoutParams={width="
+ String.valueOf(width) + ", height=" + String.valueOf(height)
+ " x=" + x + " y=" + y + "}";
}
}
}
@@ -7,14 +7,26 @@ package android.widget;
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
public class EditText { public class EditText extends TextView {
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); } public EditText(android.content.Context context) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); } public EditText(android.content.Context context, android.util.AttributeSet attrs) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); } public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); } public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
throw new RuntimeException("Stub!");
}
public boolean getFreezesText() { throw new RuntimeException("Stub!"); } public boolean getFreezesText() { throw new RuntimeException("Stub!"); }
@@ -10,6 +10,9 @@ package androidx.preference;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
@@ -22,7 +25,8 @@ public class Preference {
@JsonIgnore @JsonIgnore
protected Context context; protected Context context;
private boolean isVisible; private boolean isVisible = true;
private boolean isEnabled = true;
private String key; private String key;
private CharSequence title; private CharSequence title;
private CharSequence summary; private CharSequence summary;
@@ -68,7 +72,11 @@ public class Preference {
} }
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
throw new RuntimeException("Stub!"); isEnabled = enabled;
}
public boolean isEnabled() {
return isEnabled;
} }
public String getKey() { public String getKey() {
@@ -125,33 +133,34 @@ public class Preference {
/** Tachidesk specific API */ /** Tachidesk specific API */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Object getCurrentValue() { public Object getCurrentValue() {
switch (getDefaultValueType()) { if (key == null) {
case "String": return Objects.requireNonNullElseGet(defaultValue, () -> switch (getDefaultValueType()) {
return sharedPreferences.getString(key, (String)defaultValue); case "String" -> "";
case "Boolean": case "Boolean" -> false;
return sharedPreferences.getBoolean(key, (Boolean)defaultValue); case "Set<String>" -> new HashSet<>();
case "Set<String>": default -> throw new RuntimeException("Unsupported type");
return sharedPreferences.getStringSet(key, (Set<String>)defaultValue); });
default:
throw new RuntimeException("Unsupported type");
} }
return switch (getDefaultValueType()) {
case "String" -> sharedPreferences.getString(key, (String) defaultValue);
case "Boolean" -> sharedPreferences.getBoolean(key, (Boolean) defaultValue);
case "Set<String>" -> sharedPreferences.getStringSet(key, (Set<String>) defaultValue);
default -> throw new RuntimeException("Unsupported type");
};
} }
/** Tachidesk specific API */ /** Tachidesk specific API */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void saveNewValue(Object value) { public void saveNewValue(Object value) {
if (key == null) {
return;
}
switch (getDefaultValueType()) { switch (getDefaultValueType()) {
case "String": case "String" -> sharedPreferences.edit().putString(key, (String) value).apply();
sharedPreferences.edit().putString(key, (String)value).apply(); case "Boolean" -> sharedPreferences.edit().putBoolean(key, (Boolean) value).apply();
break; case "Set<String>" ->
case "Boolean":
sharedPreferences.edit().putBoolean(key, (Boolean)value).apply();
break;
case "Set<String>":
sharedPreferences.edit().putStringSet(key, (Set<String>) value).apply(); sharedPreferences.edit().putStringSet(key, (Set<String>) value).apply();
break; default -> throw new RuntimeException("Unsupported type");
default:
throw new RuntimeException("Unsupported type");
} }
} }
} }
@@ -11,7 +11,8 @@ import android.content.Context;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
public class TwoStatePreference extends Preference { public class TwoStatePreference extends Preference {
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them private CharSequence mSummaryOn;
private CharSequence mSummaryOff;
public TwoStatePreference(Context context) { public TwoStatePreference(Context context) {
super(context); super(context);
@@ -25,16 +26,41 @@ public class TwoStatePreference extends Preference {
public void setChecked(boolean checked) { throw new RuntimeException("Stub!"); } public void setChecked(boolean checked) { throw new RuntimeException("Stub!"); }
@JsonIgnore @JsonIgnore
public CharSequence getSummaryOn() { throw new RuntimeException("Stub!"); } public CharSequence getSummaryOn() {
return mSummaryOn;
}
@JsonIgnore @JsonIgnore
public void setSummaryOn(CharSequence summary) { throw new RuntimeException("Stub!"); } public void setSummaryOn(CharSequence summary) {
this.mSummaryOn = summary;
}
@JsonIgnore @JsonIgnore
public CharSequence getSummaryOff() { throw new RuntimeException("Stub!"); } public CharSequence getSummaryOff() {
return mSummaryOff;
}
@JsonIgnore @JsonIgnore
public void setSummaryOff(CharSequence summary) { throw new RuntimeException("Stub!"); } public void setSummaryOff(CharSequence summary) {
this.mSummaryOff = summary;
}
@Override
public CharSequence getSummary() {
final CharSequence summary = super.getSummary();
if (summary != null) {
return summary;
}
final boolean checked = (Boolean) getCurrentValue();
if (checked && mSummaryOn != null) {
return mSummaryOn;
} else if (!checked && mSummaryOff != null) {
return mSummaryOff;
}
return null;
}
@JsonIgnore @JsonIgnore
public boolean getDisableDependentsState() { throw new RuntimeException("Stub!"); } public boolean getDisableDependentsState() { throw new RuntimeException("Stub!"); }
@@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.internal.util; package com.android.internal.util;
import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.util.ArraySet; import android.util.ArraySet;
import libcore.util.EmptyArray;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.*; import java.util.*;
import libcore.util.EmptyArray;
import android.annotation.NonNull;
/** /**
* ArrayUtils contains some methods that you can call to find out * ArrayUtils contains some methods that you can call to find out
* the most efficient increments by which to grow arrays. * the most efficient increments by which to grow arrays.
@@ -50,6 +50,10 @@ public class ArrayUtils {
public static Object[] newUnpaddedObjectArray(int minLen) { public static Object[] newUnpaddedObjectArray(int minLen) {
return new Object[minLen]; return new Object[minLen];
} }
@SuppressWarnings("unchecked")
public static <T> T[] newUnpaddedArray(Class<T> clazz, int minLen) {
return (T[])Array.newInstance(clazz, minLen);
}
/** /**
* Checks if the beginnings of two byte arrays are equal. * Checks if the beginnings of two byte arrays are equal.
* *
@@ -0,0 +1,155 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.util;
public final class GrowingArrayUtils {
public static <T> T[] append(T[] array, int currentSize, T element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
@SuppressWarnings("unchecked")
T[] newArray = ArrayUtils.newUnpaddedArray(
(Class<T>) array.getClass().getComponentType(), growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static int[] append(int[] array, int currentSize, int element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static long[] append(long[] array, int currentSize, long element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static boolean[] append(boolean[] array, int currentSize, boolean element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static float[] append(float[] array, int currentSize, float element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
float[] newArray = ArrayUtils.newUnpaddedFloatArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
@SuppressWarnings("unchecked")
T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static long[] insert(long[] array, int currentSize, int index, long element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
long[] newArray = ArrayUtils.newUnpaddedLongArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static boolean[] insert(boolean[] array, int currentSize, int index, boolean element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
boolean[] newArray = ArrayUtils.newUnpaddedBooleanArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
// Uninstantiable
private GrowingArrayUtils() {}
}
@@ -0,0 +1,181 @@
@file:Suppress("UNUSED")
package kotlinx.coroutines.android
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.NonDisposableHandle
import kotlinx.coroutines.cancel
import kotlinx.coroutines.internal.MainDispatcherFactory
import kotlin.coroutines.CoroutineContext
/**
* Dispatches execution onto Android [Handler].
*
* This class provides type-safety and a point for future extensions.
*/
@OptIn(InternalCoroutinesApi::class)
public sealed class HandlerDispatcher :
MainCoroutineDispatcher(),
Delay {
/**
* Returns dispatcher that executes coroutines immediately when it is already in the right context
* (current looper is the same as this handler's looper) without an additional [re-dispatch][CoroutineDispatcher.dispatch].
* This dispatcher does not use [Handler.post] when current looper is the same as looper of the handler.
*
* Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
* The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.
*
* Example of usage:
* ```
* suspend fun updateUiElement(val text: String) {
* /*
* * If it is known that updateUiElement can be invoked both from the Main thread and from other threads,
* * `immediate` dispatcher is used as a performance optimization to avoid unnecessary dispatch.
* *
* * In that case, when `updateUiElement` is invoked from the Main thread, `uiElement.text` will be
* * invoked immediately without any dispatching, otherwise, the `Dispatchers.Main` dispatch cycle via
* * `Handler.post` will be triggered.
* */
* withContext(Dispatchers.Main.immediate) {
* uiElement.text = text
* }
* // Do context-independent logic such as logging
* }
* ```
*/
public abstract override val immediate: HandlerDispatcher
}
@OptIn(InternalCoroutinesApi::class)
internal class AndroidDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
return HandlerContext(mainLooper.asHandler())
}
override fun hintOnError(): String = "For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used"
override val loadPriority: Int
get() = Int.MAX_VALUE / 2
}
/**
* Represents an arbitrary [Handler] as an implementation of [CoroutineDispatcher]
* with an optional [name] for nicer debugging
*
* ## Rejected execution
*
* If the underlying handler is closed and its message-scheduling methods start to return `false` on
* an attempt to submit a continuation task to the resulting dispatcher,
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
*/
@JvmName("from") // this is for a nice Java API, see issue #255
@JvmOverloads
public fun Handler.asCoroutineDispatcher(name: String? = null): HandlerDispatcher =
HandlerContext(this, name)
private const val MAX_DELAY = Long.MAX_VALUE / 2 // cannot delay for too long on Android
internal fun Looper.asHandler(): Handler = Handler(this)
@JvmField
@Deprecated("Use Dispatchers.Main instead", level = DeprecationLevel.HIDDEN)
internal val Main: HandlerDispatcher? = runCatching { HandlerContext(Looper.getMainLooper().asHandler()) }.getOrNull()
/**
* Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
*/
@OptIn(InternalCoroutinesApi::class)
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean,
) : HandlerDispatcher(),
Delay {
/**
* Creates [CoroutineDispatcher] for the given Android [handler].
*
* @param handler a handler.
* @param name an optional name for debugging.
*/
constructor(
handler: Handler,
name: String? = null,
) : this(handler, name, false)
override val immediate: HandlerContext =
if (invokeImmediately) {
this
} else {
HandlerContext(handler, name, true)
}
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately || Looper.myLooper() != handler.looper
override fun dispatch(
context: CoroutineContext,
block: Runnable,
) {
if (!handler.post(block)) {
cancelOnRejection(context, block)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun scheduleResumeAfterDelay(
timeMillis: Long,
continuation: CancellableContinuation<Unit>,
) {
val block =
Runnable {
with(continuation) { resumeUndispatched(Unit) }
}
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
continuation.invokeOnCancellation { handler.removeCallbacks(block) }
} else {
cancelOnRejection(continuation.context, block)
}
}
override fun invokeOnTimeout(
timeMillis: Long,
block: Runnable,
context: CoroutineContext,
): DisposableHandle {
if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
return DisposableHandle { handler.removeCallbacks(block) }
}
cancelOnRejection(context, block)
return NonDisposableHandle
}
private fun cancelOnRejection(
context: CoroutineContext,
block: Runnable,
) {
context.cancel(CancellationException("The task was rejected, the handler underlying the dispatcher '${toString()}' was closed"))
Dispatchers.IO.dispatch(context, block)
}
override fun toString(): String =
toStringInternalImpl() ?: run {
val str = name ?: handler.toString()
if (invokeImmediately) "$str.immediate" else str
}
override fun equals(other: Any?): Boolean =
other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately
// inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer
override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237
}
@@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
package libcore.net; package libcore.net;
import android.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@@ -249,10 +251,12 @@ public final class MimeUtils {
add("audio/x-scpls", "pls"); add("audio/x-scpls", "pls");
add("audio/x-sd2", "sd2"); add("audio/x-sd2", "sd2");
add("audio/x-wav", "wav"); add("audio/x-wav", "wav");
add("image/avif", "avif");
// image/bmp isn't IANA, so image/x-ms-bmp should come first. // image/bmp isn't IANA, so image/x-ms-bmp should come first.
add("image/x-ms-bmp", "bmp"); add("image/x-ms-bmp", "bmp");
add("image/bmp", "bmp"); add("image/bmp", "bmp");
add("image/gif", "gif"); add("image/gif", "gif");
add("image/heif", "heif");
// image/ico isn't IANA, so image/x-icon should come first. // image/ico isn't IANA, so image/x-icon should come first.
add("image/x-icon", "ico"); add("image/x-icon", "ico");
add("image/ico", "cur"); add("image/ico", "cur");
@@ -262,6 +266,7 @@ public final class MimeUtils {
add("image/jpeg", "jpg"); add("image/jpeg", "jpg");
add("image/jpeg", "jpeg"); add("image/jpeg", "jpeg");
add("image/jpeg", "jpe"); add("image/jpeg", "jpe");
add("image/jxl", "jxl");
add("image/pcx", "pcx"); add("image/pcx", "pcx");
add("image/png", "png"); add("image/png", "png");
add("image/svg+xml", "svg"); add("image/svg+xml", "svg");
@@ -438,6 +443,7 @@ public final class MimeUtils {
* @return The extension has been registered for * @return The extension has been registered for
* the given case insensitive MIME type or null if there is none. * the given case insensitive MIME type or null if there is none.
*/ */
@Nullable
public static String guessExtensionFromMimeType(String mimeType) { public static String guessExtensionFromMimeType(String mimeType) {
if (mimeType == null || mimeType.isEmpty()) { if (mimeType == null || mimeType.isEmpty()) {
return null; return null;
@@ -1,8 +1,10 @@
package xyz.nulldev.androidcompat package xyz.nulldev.androidcompat
import android.webkit.WebView
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.androidcompat.config.FilesConfigModule import xyz.nulldev.androidcompat.config.FilesConfigModule
import xyz.nulldev.androidcompat.config.SystemConfigModule import xyz.nulldev.androidcompat.config.SystemConfigModule
import xyz.nulldev.androidcompat.webkit.KcefWebViewProvider
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
/** /**
@@ -17,6 +19,8 @@ class AndroidCompatInitializer {
SystemConfigModule.register(GlobalConfigManager.config), SystemConfigModule.register(GlobalConfigManager.config),
) )
WebView.setProviderFactory({ view: WebView -> KcefWebViewProvider(view) })
// Set some properties extensions use // Set some properties extensions use
System.setProperty( System.setProperty(
"http.agent", "http.agent",
@@ -0,0 +1,5 @@
package xyz.nulldev.androidcompat
fun interface CallableArgument<A, R> {
fun call(arg: A): R
}
@@ -96,7 +96,7 @@ class JavaSharedPreferences(
} else { } else {
preferences.decodeValueOrNull(SetSerializer(String.serializer()), key) preferences.decodeValueOrNull(SetSerializer(String.serializer()), key)
} }
} catch (e: SerializationException) { } catch (_: SerializationException) {
throw ClassCastException("$key was not a StringSet") throw ClassCastException("$key was not a StringSet")
} }
} }
@@ -153,10 +153,11 @@ class JavaSharedPreferences(
key: String, key: String,
value: String?, value: String?,
): SharedPreferences.Editor { ): SharedPreferences.Editor {
actions +=
if (value != null) { if (value != null) {
actions += Action.Add(key, value) Action.Add(key, value)
} else { } else {
actions += Action.Remove(key) Action.Remove(key)
} }
return this return this
} }
@@ -165,10 +166,11 @@ class JavaSharedPreferences(
key: String, key: String,
values: MutableSet<String>?, values: MutableSet<String>?,
): SharedPreferences.Editor { ): SharedPreferences.Editor {
actions +=
if (values != null) { if (values != null) {
actions += Action.Add(key, values) Action.Add(key, values)
} else { } else {
actions += Action.Remove(key) Action.Remove(key)
} }
return this return this
} }
@@ -240,13 +242,14 @@ class JavaSharedPreferences(
} }
notify(it.key) notify(it.key)
} }
is Action.Remove -> { is Action.Remove -> {
preferences.remove(it.key) preferences.remove(it.key)
/** /*
* Set<String> are stored like Set<String> are stored like
* key.0 = value1 key.0 = value1
* key.1 = value2 key.1 = value2
* key.size = 2 key.size = 2
*/ */
preferences.keys.forEach { key -> preferences.keys.forEach { key ->
if (key.startsWith(it.key + ".")) { if (key.startsWith(it.key + ".")) {
@@ -256,7 +259,10 @@ class JavaSharedPreferences(
notify(it.key) notify(it.key)
} }
Action.Clear -> preferences.clear()
Action.Clear -> {
preferences.clear()
}
} }
} }
} }
@@ -0,0 +1,429 @@
package xyz.nulldev.androidcompat.webkit
import android.webkit.WebSettings
class KcefWebSettings : WebSettings() {
// Boolean settings
private var navDumps = false
private var mediaPlaybackRequiresUserGesture = true
private var builtInZoomControls = false
private var displayZoomControls = true
private var allowFileAccess = true
private var allowContentAccess = true
private var pluginsEnabled = false
private var loadWithOverviewMode = false
private var enableSmoothTransition = false
private var useWebViewBackgroundForOverscrollBackground = false
private var saveFormData = true
private var savePassword = false
private var acceptThirdPartyCookies = true
private var lightTouchEnabled = false
private var useWideViewPort = true
private var supportMultipleWindows = false
private var loadsImagesAutomatically = true
private var blockNetworkImage = false
private var blockNetworkLoads = false
private var javaScriptEnabled = true
private var allowUniversalAccessFromFileURLs = false
private var allowFileAccessFromFileURLs = false
private var domStorageEnabled = true
private var geolocationEnabled = true
private var javaScriptCanOpenWindowsAutomatically = false
private var needInitialFocus = false
private var offscreenPreRaster = false
private var videoOverlayForEmbeddedEncryptedVideoEnabled = false
private var safeBrowsingEnabled = true
// Integer settings
private var textZoom = 100
private var minimumFontSize = 8
private var minimumLogicalFontSize = 8
private var defaultFontSize = 16
private var defaultFixedFontSize = 13
private var cacheMode = 0
private var mixedContentMode = 0
private var disabledActionModeMenuItems = 0
// String settings
private var databasePath: String? = null
private var geolocationDatabasePath: String? = null
private var appCachePath: String? = null
private var defaultTextEncodingName: String? = null
private var userAgentString: String? = null
private var standardFontFamily: String? = null
private var fixedFontFamily: String? = null
private var sansSerifFontFamily: String? = null
private var serifFontFamily: String? = null
private var cursiveFontFamily: String? = null
private var fantasyFontFamily: String? = null
// Enum settings
private var defaultZoom: ZoomDensity? = null
private var layoutAlgorithm: LayoutAlgorithm? = null
private var pluginState: PluginState? = null
private var renderPriority: RenderPriority? = null
// Long settings
private var appCacheMaxSize: Long = 0L
// Implementations
@SuppressWarnings("HiddenAbstractMethod")
@Deprecated("inherit")
override fun setNavDump(p0: Boolean) {
navDumps = p0
}
@Deprecated("inherit")
@SuppressWarnings("HiddenAbstractMethod")
override fun getNavDump(): Boolean = navDumps
override fun setSupportZoom(p0: Boolean) {
mediaPlaybackRequiresUserGesture = p0
}
override fun supportZoom() = mediaPlaybackRequiresUserGesture
override fun setMediaPlaybackRequiresUserGesture(p0: Boolean) {
mediaPlaybackRequiresUserGesture = p0
}
override fun getMediaPlaybackRequiresUserGesture() = mediaPlaybackRequiresUserGesture
override fun setBuiltInZoomControls(p0: Boolean) {
builtInZoomControls = p0
}
override fun getBuiltInZoomControls() = builtInZoomControls
override fun setDisplayZoomControls(p0: Boolean) {
displayZoomControls = p0
}
override fun getDisplayZoomControls() = displayZoomControls
override fun setAllowFileAccess(p0: Boolean) {
allowFileAccess = p0
}
override fun getAllowFileAccess() = allowFileAccess
override fun setAllowContentAccess(p0: Boolean) {
allowContentAccess = p0
}
override fun getAllowContentAccess() = allowContentAccess
override fun setLoadWithOverviewMode(p0: Boolean) {
loadWithOverviewMode = p0
}
override fun getLoadWithOverviewMode() = loadWithOverviewMode
@Deprecated("inherit")
override fun setPluginsEnabled(p0: Boolean) {
pluginsEnabled = p0
}
@Deprecated("inherit")
override fun getPluginsEnabled() = pluginsEnabled
@Deprecated("inherit")
override fun setEnableSmoothTransition(p0: Boolean) {
enableSmoothTransition = p0
}
@Deprecated("inherit")
override fun enableSmoothTransition() = enableSmoothTransition
@Deprecated("inherit")
override fun setUseWebViewBackgroundForOverscrollBackground(p0: Boolean) {
useWebViewBackgroundForOverscrollBackground = p0
}
@Deprecated("inherit")
override fun getUseWebViewBackgroundForOverscrollBackground() = useWebViewBackgroundForOverscrollBackground
@Deprecated("inherit")
override fun setSaveFormData(p0: Boolean) {
saveFormData = p0
}
@Deprecated("inherit")
override fun getSaveFormData() = saveFormData
@Deprecated("inherit")
override fun setSavePassword(p0: Boolean) {
savePassword = p0
}
@Deprecated("inherit")
override fun getSavePassword() = savePassword
override fun setTextZoom(p0: Int) {
textZoom = p0
}
override fun getTextZoom() = textZoom
@Deprecated("inherit")
override fun setDefaultZoom(p0: ZoomDensity?) {
defaultZoom = p0
}
override fun setAcceptThirdPartyCookies(p0: Boolean) {
acceptThirdPartyCookies = p0
}
override fun getAcceptThirdPartyCookies() = acceptThirdPartyCookies
@Deprecated("inherit")
override fun getDefaultZoom() = defaultZoom
@Deprecated("inherit")
override fun setLightTouchEnabled(p0: Boolean) {
lightTouchEnabled = p0
}
@Deprecated("inherit")
override fun getLightTouchEnabled() = lightTouchEnabled
@Deprecated("inherit")
override fun setUserAgent(ua: Int) = throw RuntimeException("Stub!")
@Deprecated("inherit")
override fun getUserAgent(): Int = throw RuntimeException("Stub!")
override fun setUseWideViewPort(p0: Boolean) {
useWideViewPort = p0
}
override fun getUseWideViewPort() = useWideViewPort
override fun setSupportMultipleWindows(p0: Boolean) {
supportMultipleWindows = p0
}
override fun supportMultipleWindows() = supportMultipleWindows
override fun setLayoutAlgorithm(p0: LayoutAlgorithm?) {
layoutAlgorithm = p0
}
override fun getLayoutAlgorithm() = layoutAlgorithm
override fun setStandardFontFamily(p0: String?) {
standardFontFamily = p0
}
override fun getStandardFontFamily() = standardFontFamily
override fun setFixedFontFamily(p0: String?) {
fixedFontFamily = p0
}
override fun getFixedFontFamily() = fixedFontFamily
override fun setSansSerifFontFamily(p0: String?) {
sansSerifFontFamily = p0
}
override fun getSansSerifFontFamily() = sansSerifFontFamily
override fun setSerifFontFamily(p0: String?) {
serifFontFamily = p0
}
override fun getSerifFontFamily() = serifFontFamily
override fun setCursiveFontFamily(p0: String?) {
cursiveFontFamily = p0
}
override fun getCursiveFontFamily() = cursiveFontFamily
override fun setFantasyFontFamily(p0: String?) {
fantasyFontFamily = p0
}
override fun getFantasyFontFamily() = fantasyFontFamily
override fun setMinimumFontSize(p0: Int) {
minimumFontSize = p0
}
override fun getMinimumFontSize() = minimumFontSize
override fun setMinimumLogicalFontSize(p0: Int) {
minimumLogicalFontSize = p0
}
override fun getMinimumLogicalFontSize() = minimumLogicalFontSize
override fun setDefaultFontSize(p0: Int) {
defaultFontSize = p0
}
override fun getDefaultFontSize() = defaultFontSize
override fun setDefaultFixedFontSize(p0: Int) {
defaultFixedFontSize = p0
}
override fun getDefaultFixedFontSize() = defaultFixedFontSize
override fun setLoadsImagesAutomatically(p0: Boolean) {
loadsImagesAutomatically = p0
}
override fun getLoadsImagesAutomatically() = loadsImagesAutomatically
override fun setBlockNetworkImage(p0: Boolean) {
blockNetworkImage = p0
}
override fun getBlockNetworkImage() = blockNetworkImage
override fun setBlockNetworkLoads(p0: Boolean) {
blockNetworkLoads = p0
}
override fun getBlockNetworkLoads() = blockNetworkLoads
override fun setJavaScriptEnabled(p0: Boolean) {
javaScriptEnabled = p0
}
override fun getJavaScriptEnabled() = javaScriptEnabled
@Deprecated("inherit")
override fun setAllowUniversalAccessFromFileURLs(p0: Boolean) {
allowUniversalAccessFromFileURLs = p0
}
override fun getAllowUniversalAccessFromFileURLs() = allowUniversalAccessFromFileURLs
@Deprecated("inherit")
override fun setAllowFileAccessFromFileURLs(p0: Boolean) {
allowFileAccessFromFileURLs = p0
}
override fun getAllowFileAccessFromFileURLs() = allowFileAccessFromFileURLs
@Deprecated("inherit")
override fun setPluginState(p0: PluginState?) {
pluginState = p0
}
@Deprecated("inherit")
override fun getPluginState() = pluginState
@Deprecated("inherit")
override fun setDatabasePath(p0: String?) {
databasePath = p0
}
@Deprecated("inherit")
override fun getDatabasePath() = databasePath ?: ""
@Deprecated("inherit")
override fun setGeolocationDatabasePath(p0: String?) {
geolocationDatabasePath = p0
}
@Deprecated("inherit")
override fun setAppCacheEnabled(p0: Boolean) {}
@Deprecated("inherit")
override fun setAppCachePath(p0: String?) {
appCachePath = p0
}
@Deprecated("inherit")
override fun setAppCacheMaxSize(p0: Long) {
appCacheMaxSize = p0
}
@Deprecated("inherit")
override fun setDatabaseEnabled(p0: Boolean) {}
@Deprecated("inherit")
override fun getDatabaseEnabled() = true
override fun setDomStorageEnabled(p0: Boolean) {
domStorageEnabled = p0
}
override fun getDomStorageEnabled() = domStorageEnabled
override fun setGeolocationEnabled(p0: Boolean) {
geolocationEnabled = p0
}
override fun setJavaScriptCanOpenWindowsAutomatically(p0: Boolean) {
javaScriptCanOpenWindowsAutomatically = p0
}
override fun getJavaScriptCanOpenWindowsAutomatically() = javaScriptCanOpenWindowsAutomatically
override fun setDefaultTextEncodingName(p0: String?) {
defaultTextEncodingName = p0
}
override fun getDefaultTextEncodingName() = defaultTextEncodingName ?: ""
override fun setUserAgentString(p0: String?) {
userAgentString = p0
}
override fun getUserAgentString() = userAgentString ?: defaultUserAgent()
override fun setNeedInitialFocus(p0: Boolean) {
needInitialFocus = p0
}
@Deprecated("inherit")
override fun setRenderPriority(p0: RenderPriority?) {
renderPriority = p0
}
override fun setCacheMode(p0: Int) {
cacheMode = p0
}
override fun getCacheMode() = cacheMode
override fun setMixedContentMode(p0: Int) {
mixedContentMode = p0
}
override fun getMixedContentMode() = mixedContentMode
override fun setOffscreenPreRaster(p0: Boolean) {
offscreenPreRaster = p0
}
override fun getOffscreenPreRaster() = offscreenPreRaster
override fun setVideoOverlayForEmbeddedEncryptedVideoEnabled(p0: Boolean) {
videoOverlayForEmbeddedEncryptedVideoEnabled = p0
}
override fun getVideoOverlayForEmbeddedEncryptedVideoEnabled() = videoOverlayForEmbeddedEncryptedVideoEnabled
override fun setSafeBrowsingEnabled(p0: Boolean) {
safeBrowsingEnabled = p0
}
override fun getSafeBrowsingEnabled() = safeBrowsingEnabled
override fun setDisabledActionModeMenuItems(p0: Int) {
disabledActionModeMenuItems = p0
}
override fun getDisabledActionModeMenuItems() = disabledActionModeMenuItems
companion object {
fun defaultUserAgent() = System.getProperty("http.agent")
}
}
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/symbols)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+309 -1584
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -61,7 +61,7 @@ This structure is chosen to
### Prerequisites ### Prerequisites
You need these software packages installed in order to build the project You need these software packages installed in order to build the project
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works) - Java Development Kit version 21 or newer(we suggest using Zulu or Temurin instead of Oracle JDK)
### building the full-blown jar (Suwayomi-Server + Suwayomi-WebUI bundle) ### building the full-blown jar (Suwayomi-Server + Suwayomi-WebUI bundle)
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Suwayomi-Server-vX.Y.Z-rxxx.jar`. Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Suwayomi-Server-vX.Y.Z-rxxx.jar`.
+105
View File
@@ -0,0 +1,105 @@
FROM eclipse-temurin:25.0.3_9-jdk-noble AS build
ARG TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL
# build abort handler
RUN if [ -n "$TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL" ]; then \
apt-get update && \
apt-get -y install -y curl gcc && \
cd /tmp && \
curl "$TACHIDESK_ABORT_HANDLER_DOWNLOAD_URL" -O && \
gcc -fPIC -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -shared catch_abort.c -lpthread -o /opt/catch_abort.so && \
rm -f catch_abort.c && \
apt-get -y purge gcc --auto-remove && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* || exit 1; \
fi
# Build the server jar from source
WORKDIR /app
COPY . .
RUN GRADLE_OPTS="-Xmx4g" ./gradlew :server:shadowJar --no-daemon -x test
FROM eclipse-temurin:25.0.3_9-jre-noble
ARG TARGETPLATFORM
ARG TACHIDESK_KCEF=n # y or n, leave empty for auto-detection
ARG TACHIDESK_KCEF_RELEASE_URL
# Install envsubst from GNU's gettext project
# install unzip to unzip the server-reference.conf from the jar
# Install tini for a tiny init system (handles orphan processes for graceful restart)
RUN apt-get update && \
apt-get -y install -y curl gettext-base unzip tini ca-certificates p11-kit && \
/usr/bin/p11-kit extract --format=java-cacerts --filter=certificates --overwrite --purpose server-auth $JAVA_HOME/lib/security/cacerts && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY scripts/kcef_download.sh /root/kcef_download.sh
RUN chmod +x /root/kcef_download.sh
# install CEF dependencies
RUN if [ "$TACHIDESK_KCEF" = "y" ] || ([ "$TACHIDESK_KCEF" = "" ] && ([ "$TARGETPLATFORM" = "linux/amd64" ] || [ "$TARGETPLATFORM" = "linux/arm64" ])); then \
apt-get update && \
apt-get -y install --no-install-recommends -y libxss1 libxext6 libxrender1 libxcomposite1 libxdamage1 libxkbcommon0 libxtst6 \
libjogl2-jni libgluegen2-jni libglib2.0-0t64 libnss3 libdbus-1-3 libpango-1.0-0 libcairo2 libasound2t64 \
libatk-bridge2.0-0t64 libcups2t64 libdrm2 libgbm1 xvfb \
curl jq gawk findutils && \
/root/kcef_download.sh "$TACHIDESK_KCEF_RELEASE_URL" "$TARGETPLATFORM" && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* || exit 1; \
fi
COPY --from=build /opt/*.so /opt/
# Create a user to run as
# .X11-unix must be created by root
# Ubuntu exposes libgluegen_rt.so as libgluegen2_rt.so for some reason, so rename it
# JCEF (or Java?) also does not search /usr/lib/jni, so copy them over into one it will search
RUN userdel -r ubuntu && \
groupadd --gid 1000 suwayomi && \
useradd --uid 1000 --gid suwayomi --no-log-init -G audio,video suwayomi && \
mkdir -p /home/suwayomi/.local/share/Tachidesk && \
if command -v Xvfb; then \
mkdir /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix && \
cp /usr/lib/jni/libgluegen2_rt.so /home/suwayomi/libgluegen_rt.so && \
cp /usr/lib/jni/*.so /home/suwayomi/; \
fi
COPY scripts/create_server_conf.sh /home/suwayomi/create_server_conf.sh
COPY scripts/startup_script.sh /home/suwayomi/startup_script.sh
RUN chmod +x /home/suwayomi/create_server_conf.sh /home/suwayomi/startup_script.sh
# Copy locally built jar; grant o+rwx so non-default UIDs can write server.conf
RUN mkdir -p /home/suwayomi/startup
COPY --from=build /app/server/build/*.jar /home/suwayomi/startup/tachidesk_latest.jar
RUN chmod 777 -R /home/suwayomi && \
chown -R suwayomi:suwayomi /home/suwayomi
ARG BUILD_DATE
ARG TACHIDESK_RELEASE_TAG
ARG TACHIDESK_FILENAME
ARG TACHIDESK_DOCKER_GIT_COMMIT
LABEL maintainer="suwayomi" \
org.opencontainers.image.title="Suwayomi Docker" \
org.opencontainers.image.authors="https://github.com/suwayomi" \
org.opencontainers.image.url="https://github.com/suwayomi/docker-tachidesk/pkgs/container/tachidesk" \
org.opencontainers.image.source="https://github.com/suwayomi/docker-tachidesk" \
org.opencontainers.image.description="This image is used to start suwayomi server in a container" \
org.opencontainers.image.vendor="suwayomi" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$TACHIDESK_RELEASE_TAG \
tachidesk.docker_commit=$TACHIDESK_DOCKER_GIT_COMMIT \
tachidesk.release_tag=$TACHIDESK_RELEASE_TAG \
tachidesk.filename=$TACHIDESK_FILENAME \
org.opencontainers.image.licenses="MPL-2.0"
ENV HOME=/home/suwayomi
WORKDIR /home/suwayomi
USER suwayomi
EXPOSE 4567
ENTRYPOINT ["tini", "--"]
CMD ["/home/suwayomi/startup_script.sh"]
# vim: set ft=dockerfile:
+96 -45
View File
@@ -3,28 +3,35 @@
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| |-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| ![CI](https://github.com/Suwayomi/Suwayomi-Server/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Suwayomi-Server.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Suwayomi-Server/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Suwayomi-Server-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | | ![CI](https://github.com/Suwayomi/Suwayomi-Server/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Suwayomi-Server.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Suwayomi-Server/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Suwayomi-Server-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
## Table of Content ## Table of Contents
- [What is Suwayomi?](#what-is-suwayomi) - [What is Suwayomi?](#what-is-suwayomi)
- [Features](#Features) - [Features](#features)
- [Suwayomi client projects](#Suwayomi-client-projects) - [Suwayomi client projects](#suwayomi-client-projects)
- [Integrated clients](#integrated-clients)
- [Other clients](#other-clients-potentially-inactive-or-abondend)
- [Downloading and Running the app](#downloading-and-running-the-app) - [Downloading and Running the app](#downloading-and-running-the-app)
* [Using Operating System Specific Bundles](#using-operating-system-specific-bundles) - [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
- [Launcher Scripts](#launcher-scripts) - [Windows](#windows)
+ [Windows](#windows) - [macOS](#macos)
+ [macOS](#macos) - [GNU/Linux](#gnulinux)
+ [GNU/Linux](#gnulinux) - [Other methods of getting Suwayomi](#other-methods-of-getting-suwayomi)
* [Other methods of getting Suwayomi](#other-methods-of-getting-suwayomi) - [Docker](#docker)
+ [Arch Linux](#arch-linux) - [Arch Linux](#arch-linux)
+ [Ubuntu-based distributions](#ubuntu-based-distributions) - [Debian/Ubuntu](#debianubuntu)
+ [Docker](#docker) - [NixOS](#nixos)
* [Advanced Methods](#advanced-methods) - [Advanced Methods](#advanced-methods)
+ [Running the jar release directly](#running-the-jar-release-directly) - [Running the jar release directly](#running-the-jar-release-directly)
+ [Using Suwayomi Remotely](#using-suwayomi-remotely) - [Using Suwayomi Remotely](#using-suwayomi-remotely)
- [Syncing With Mihon (Tachiyomi)](#syncing-with-mihon-tachiyomi) - [Syncing With Mihon (Tachiyomi) and Neko](#syncing-with-mihon-tachiyomi-and-neko)
- [The Suwayomi extension and tracker](#the-suwayomi-extension-and-tracker)
- [The Suwayomi merge source in Neko](#the-suwayomi-merge-source-in-neko)
- [Other methods](#other-methods)
- [Troubleshooting and Support](#troubleshooting-and-support) - [Troubleshooting and Support](#troubleshooting-and-support)
- [Contributing and Technical info](#contributing-and-technical-info) - [Contributing and Technical info](#contributing-and-technical-info)
- [Translation](#translation)
- [Credit](#credit) - [Credit](#credit)
- [License](#license) - [License](#license)
- [Disclaimer](#disclaimer)
<!-- Generated with https://ecotrust-canada.github.io/markdown-toc/ --> <!-- Generated with https://ecotrust-canada.github.io/markdown-toc/ -->
# What is Suwayomi? # What is Suwayomi?
@@ -36,7 +43,7 @@ Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a
Suwayomi-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions. Suwayomi-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look [here](#syncing-with-mihon-tachiyomi). You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look [here](#syncing-with-mihon-tachiyomi-and-neko).
## Features ## Features
> [!NOTE] > [!NOTE]
@@ -52,25 +59,29 @@ You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look
- Ability to download Manga for offline read - Ability to download Manga for offline read
- Backup and restore support powered by Mihon (Tachiyomi)-compatible Backups - Backup and restore support powered by Mihon (Tachiyomi)-compatible Backups
- Automated backup creations - Automated backup creations
- Tracking via [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [MangaUpdates](https://www.mangaupdates.com/) - Tracking via [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [MangaUpdates](https://www.mangaupdates.com/), etc.
- [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) support to bypass Cloudflare protection - [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) support to bypass Cloudflare protection
- Automated WebUI updates (supports the default WebUI and VUI) - Automated WebUI updates (supports the default WebUI and VUI)
- OPDS and OPDS-PSE support (endpoint: `/api/opds/v1.2`)
# Suwayomi client projects # Suwayomi Client Projects
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.** **You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features): Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
##### Actively Developed Clients
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default. ##### Integrated clients
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin. These clients are built-in options, and the server can keep them automatically up-to-date.
##### Inactive Clients (functional but outdated)
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server. - [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): Web app, PWA
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Mihon (Tachiyomi). - [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): Web app, PWA
##### Abandoned Clients (functionality unknown)
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic. ##### Other clients (potentially inactive or abandoned)
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client. - [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): Desktop app (windows, linux, mac); UI in the browser, manages its own suwayomi server instance
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js. - [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), can manage its own suwayomi server instance
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): Desktop app (windows, linux, mac); can manage its own suwayomi server instance
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): Web app; Desktop app (windows, linux, mac); Android app; requires access to a running server
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): Android app; iOS app Desktop app (linux); requires access to a running server
# Downloading and Running the app # Downloading and Running the app
## Using Operating System Specific Bundles ## Using Operating System Specific Bundles
@@ -93,6 +104,25 @@ Download the latest `linux-x64`(x86_64) release from [the releases section](http
`tar xvf` the downloaded file and double-click on one of the launcher scripts or run them using the terminal. `tar xvf` the downloaded file and double-click on one of the launcher scripts or run them using the terminal.
#### WebView support (GNU/Linux)
WebView support is implemented via [KCEF](https://github.com/DATL4G/KCEF).
This is optional, and is only necessary to support some extensions.
To have a functional WebView, several dependencies are required; aside from X11 libraries necessary for rendering Chromium, some JNI bindings are necessary: gluegen and jogl (found in Ubuntu as `libgluegen2-jni` and `libjogl2-jni`).
Note that on some systems (e.g. Ubuntu), the JNI libraries are not automatically found, see below.
A KCEF server is launched on startup, which loads the X11 libraries.
If those are missing, you should see "Could not load 'jcef' library".
If so, use `ldd ~/.local/share/Tachidesk/bin/kcef/libjcef.so | grep not` to figure out which libraries are not found on your system.
The JNI bindings are only loaded when a browser is actually launched.
This is done by extensions that rely on WebView, not by Suwayomi itself.
If there is a problem loading the JNI libraries, you should see a message indicating the library and the search path.
This search path includes the current working directory, if you do not want to modify system directories.
Refer to the [Dockerfile](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/Dockerfile) for more details.
## Other methods of getting Suwayomi ## Other methods of getting Suwayomi
### Docker ### Docker
Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser. Check our Official Docker release [Suwayomi Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Suwayomi Server in a docker container. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk), an example compose file can also be found there. By default, the server will be running on http://localhost:4567 open this url in your browser.
@@ -100,23 +130,26 @@ Check our Official Docker release [Suwayomi Container](https://github.com/orgs/S
### Arch Linux ### Arch Linux
You can install Suwayomi from the AUR: You can install Suwayomi from the AUR:
``` ```
yay -S tachidesk yay -S suwayomi-server-bin
``` ```
### Debian/Ubuntu ### Debian/Ubuntu
Download the latest deb package from the release section or Install from the MPR Download the latest deb package from the release section.
```
git clone https://mpr.makedeb.org/suwayomi-server.git
cd suwayomi-server
makedeb -si
```
### Ubuntu > [!CAUTION]
``` > These options are outdated and unmaintained ([relevant issue](https://github.com/Suwayomi/Suwayomi-Server/issues/1318))
sudo add-apt-repository ppa:suwayomi/suwayomi-server > ### MPR
sudo apt update > ```
sudo apt install suwayomi-server > git clone https://mpr.makedeb.org/tachidesk-server.git
``` > cd tachidesk-server
> makedeb -si
> ```
> ### Ubuntu
> ```
> sudo add-apt-repository ppa:suwayomi/tachidesk-server
> sudo apt update
> sudo apt install tachidesk-server
> ```
### NixOS ### NixOS
You can deploy Suwayomi on NixOS using the module `services.suwayomi-server` in your configuration: You can deploy Suwayomi on NixOS using the module `services.suwayomi-server` in your configuration:
@@ -152,9 +185,9 @@ Check out [this wiki page](https://github.com/Suwayomi/Suwayomi-Server/wiki/Conf
If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click-clack away!). If you face issues with your setup then we are happy to provide help, just join our discord server(a discord badge is on the top of the page, you are just a click-clack away!).
## Syncing With Mihon (Tachiyomi) ## Syncing With Mihon (Tachiyomi) and Neko
### The Suwayomi extension and tracker ### The Suwayomi extension and tracker
- You can install the `Suwayomi` extension inside Mihon (Tachiyomi). - You can install and configure the `Suwayomi` [extension](https://github.com/Suwayomi/tachiyomi-extension) inside Mihon (Tachiyomi) and forks.
- The extension will load your Suwayomi library. - The extension will load your Suwayomi library.
- By manipulating extension search filters you can browse your categories. - By manipulating extension search filters you can browse your categories.
- You can enable the Suwayomi tracker to track reading progress with your Suwayomi server. - You can enable the Suwayomi tracker to track reading progress with your Suwayomi server.
@@ -162,6 +195,15 @@ If you face issues with your setup then we are happy to provide help, just join
- Mihon (Tachiyomi) to Suwayomi: Mihon (Tachiyomi) automatically updates the chapters read status when it's updating the tracker (e.g. while reading) - Mihon (Tachiyomi) to Suwayomi: Mihon (Tachiyomi) automatically updates the chapters read status when it's updating the tracker (e.g. while reading)
- Suwayomi to Mihon (Tachiyomi): To sync Mihon (Tachiyomi) with Suwayomi, you have to open the manga's track information, then, Mihon (Tachiyomi) will automatically update its chapter list with the state from Suwayomi - Suwayomi to Mihon (Tachiyomi): To sync Mihon (Tachiyomi) with Suwayomi, you have to open the manga's track information, then, Mihon (Tachiyomi) will automatically update its chapter list with the state from Suwayomi
### The Suwayomi merge source in Neko
- You can enable the `Suwayomi` source in the Merge Source settings
- You can merge titles in Neko with titles from your Suwayomi library.
- You can enable 2-way automatic sync to track reading progress with your Suwayomi server.
- Note: only applies to merged titles
- Neko automatically updates the chapters read status in Suwayomi
- During updates, Neko will automatically update its chapter list with the read state from Suwayomi
- This only pulls if the status is read, to prevent marking read chapters as unread in Neko
### Other methods ### Other methods
Checkout [this issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) for tracking progress. Checkout [this issue](https://github.com/Suwayomi/Suwayomi-Server/issues/159) for tracking progress.
@@ -171,6 +213,15 @@ See [this troubleshooting wiki page](https://github.com/Suwayomi/Suwayomi-Server
## Contributing and Technical info ## Contributing and Technical info
See [CONTRIBUTING.md](./CONTRIBUTING.md). See [CONTRIBUTING.md](./CONTRIBUTING.md).
## Translation
Feel free to translate the project on [Weblate](https://hosted.weblate.org/projects/suwayomi/suwayomi-server/)
<details><summary>Translation Progress</summary>
<a href="https://hosted.weblate.org/engage/suwayomi-server/">
<img src="https://hosted.weblate.org/widgets/suwayomi/-/suwayomi-server/multi-auto.svg" alt="Translation status" />
</a>
</details>
## Credit ## Credit
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb. This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
+14 -7
View File
@@ -4,11 +4,14 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension
import org.jlleitschuh.gradle.ktlint.KtlintPlugin import org.jlleitschuh.gradle.ktlint.KtlintPlugin
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ktlint) alias(libs.plugins.ktlint) apply false
alias(libs.plugins.buildconfig) apply false alias(libs.plugins.buildconfig) apply false
alias(libs.plugins.download) alias(libs.plugins.download)
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.moko) apply false
alias(libs.plugins.jte) apply false
} }
allprojects { allprojects {
@@ -21,14 +24,16 @@ allprojects {
google() google()
maven("https://github.com/Suwayomi/Suwayomi-Server/raw/android-jar/") maven("https://github.com/Suwayomi/Suwayomi-Server/raw/android-jar/")
maven("https://jitpack.io") maven("https://jitpack.io")
maven("https://jogamp.org/deployment/maven")
} }
} }
subprojects { subprojects {
plugins.withType<JavaPlugin> { plugins.withType<JavaPlugin> {
extensions.configure<JavaPluginExtension> { extensions.configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_21 val javaVersion = JavaVersion.toVersion(libs.versions.jvmTarget.get())
targetCompatibility = JavaVersion.VERSION_21 sourceCompatibility = javaVersion
targetCompatibility = javaVersion
} }
} }
@@ -43,10 +48,12 @@ subprojects {
tasks { tasks {
withType<KotlinJvmCompile> { withType<KotlinJvmCompile> {
if (plugins.hasPlugin(KtlintPlugin::class)) {
dependsOn("ktlintFormat") dependsOn("ktlintFormat")
}
compilerOptions { compilerOptions {
jvmTarget = JvmTarget.JVM_21 jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
freeCompilerArgs.add("-Xcontext-receivers") freeCompilerArgs.add("-Xcontext-parameters")
} }
} }
} }
+12
View File
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
`kotlin-dsl` `kotlin-dsl`
} }
@@ -9,3 +11,13 @@ repositories {
dependencies { dependencies {
implementation(libs.zip4j) implementation(libs.zip4j)
} }
java {
val javaVersion = JavaVersion.toVersion(libs.versions.jvmTarget.get())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions.jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
}
+2 -2
View File
@@ -10,9 +10,9 @@ import java.io.BufferedReader
const val MainClass = "suwayomi.tachidesk.MainKt" const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val getTachideskVersion = { "v2.0.${getCommitCount()}" } val getTachideskVersion = { "v2.2.${getCommitCount()}" }
val webUIRevisionTag = "r2467" val webUIRevisionTag = "r3136"
private val getCommitCount = { private val getCommitCount = {
runCatching { runCatching {
+71
View File
@@ -0,0 +1,71 @@
---
services:
suwayomi:
build: .
platform: linux/amd64
image: registry.achmad.dev/suwayomi-server:latest
# user: 1000:1000
environment:
- TZ=${TZ:-Etc/UTC} # Add a TZ variable to .env to change it
- DATABASE_TYPE=POSTGRESQL
- DATABASE_URL=postgresql://postgresql:5432/${POSTGRES_DB}
- DATABASE_USERNAME=${POSTGRES_USER}
- DATABASE_PASSWORD=${POSTGRES_PASSWORD}
# - USE_HIKARI_CONNECTION_POOL=false # Hikari Connection Pool can cause issues with some installations, but it is much more performant.
# Comment these out if you do not use the flaresolverr container at the bottom of this file
- FLARESOLVERR_ENABLED=true
- FLARESOLVERR_URL=http://flaresolverr:8191
# #################################################################################################
#
# !!! IMPORTANT !!!
# - server settings can be changed during runtime in the WebUI
# - providing an environment variable will OVERWRITE the current setting value when starting the container
#
# #################################################################################################
#
# example for setting env vars:
#
# - BIND_IP=0.0.0.0
# - BIND_PORT=4567
# - SOCKS_PROXY_ENABLED=false
# - DOWNLOAD_AS_CBZ=true
# - AUTH_MODE=basic_auth
# - AUTH_USERNAME=manga
# - AUTH_PASSWORD=hello123
# - EXTENSION_REPOS=["http://github.com/orginazation-name/repo-name", "http://github.com/orginazation-name-2/repo-name-2"]
depends_on:
postgresql:
condition: service_healthy
restart: true
volumes:
- ./data:/home/suwayomi/.local/share/Tachidesk
ports:
- "4567:4567"
restart: on-failure:3
postgresql:
image: postgres:18.3
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- PGDATA=/data/postgres
- POSTGRES_DB=${POSTGRES_DB}
- TZ=${TZ:-Etc/UTC}
- PGTZ=${TZ:-Etc/UTC}
volumes:
- ./postgres:/data/postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
flaresolverr:
image: ghcr.io/thephaseless/byparr:latest
container_name: flaresolverr
init: true
environment:
- TZ=${TZ:-Etc/UTC}
restart: unless-stopped
+43
View File
@@ -0,0 +1,43 @@
---
services:
suwayomi:
build: .
platform: linux/amd64
image: registry.achmad.dev/suwayomi-server:latest
# user: 1000:1000
environment:
- TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Comment these out if you do not use the flaresolverr container at the bottom of this file
- FLARESOLVERR_ENABLED=true
- FLARESOLVERR_URL=http://flaresolverr:8191
# #################################################################################################
#
# !!! IMPORTANT !!!
# - server settings can be changed during runtime in the WebUI
# - providing an environment variable will OVERWRITE the current setting value when starting the container
#
# #################################################################################################
#
# example for setting env vars:
#
# - BIND_IP=0.0.0.0
# - BIND_PORT=4567
# - SOCKS_PROXY_ENABLED=false
# - DOWNLOAD_AS_CBZ=true
# - AUTH_MODE=basic_auth
# - AUTH_USERNAME=manga
# - AUTH_PASSWORD=hello123
# - EXTENSION_REPOS=["http://github.com/orginazation-name/repo-name", "http://github.com/orginazation-name-2/repo-name-2"]
volumes:
- ./data:/home/suwayomi/.local/share/Tachidesk
ports:
- "4567:4567"
restart: on-failure:3
flaresolverr:
image: ghcr.io/thephaseless/byparr:latest
container_name: flaresolverr
init: true
environment:
- TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
restart: unless-stopped
+298
View File
@@ -0,0 +1,298 @@
Suwayomi-Server configuration file is named `server.conf` and is located inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory).
The configuration file is written in HOCON. Google is your friend if you want to know more.
**Note:** new keys might be added in the future. Suwayomi generally attempts to update your conf file automatically, but some keys may need to be updated manually.
## Troubleshooting
### I messed up my configuration file
Suwayomi will create a default configuration file when one doesn't exist, you can delete `server.conf` to get a copy of the reference configuration file after a restart.
### I am running Suwayomi in a headless environment (docker, NAS, VPS, etc.)
- Set `server.systemTrayEnabled` to false, it will prevent Suwayomi to attempt to create a System Tray icon.
- Set `server.initialOpenInBrowserEnabled`to false, it will prevent Suwayomi to attempt to open a browser on startup.
### My Suwayomi data directory/downloads size is getting to big
- Set `server.downloadsPath` to the desired path, if you only need to change where downloads are stored. You have to move/remove the existing downloads manually.
- Set the special `server.rootDir` key if you need Suwayomi to use a custom data directory path, refer to [this section](#overriding-tachidesk-servers-data-directory-path).
## Configuration Options
### Server ip and port bindings
```conf
server.ip = "0.0.0.0"
server.port = 4567
```
- `server.ip` can be an IP or domain name.
### Socks5 proxy
```
server.socksProxyEnabled = false
server.socksProxyVersion = 5 # 4 or 5
server.socksProxyHost = ""
server.socksProxyPort = ""
server.socksProxyUsername = ""
server.socksProxyPassword = ""
```
This section directs Suwayomi to connect to the network through a proxy server.
An example configuration can be:
```
server.socksProxyEnabled = true
server.socksProxyHost = "yourproxyhost.com"
server.socksProxyPort = "8080"
```
### webUI
```
server.webUIEnabled = true
server.initialOpenInBrowserEnabled = true
server.webUIInterface = "browser" # "browser" or "electron"
server.electronPath = ""
server.webUIFlavor = "WebUI" # "WebUI" or "Custom"
server.webUIChannel = preview # "BUNDLED" or "STABLE" or "PREVIEW"
server.webUIUpdateCheckInterval = 23
server.webUISubpath = ""
```
- `server.webUIEnabled` controls if Suwayomi will serve `Suwayomi-WebUI` and if it downloads/updates it on startup.
- `server.initialOpenInBrowserEnabled` controls if Suwayomi will attempt to open a brwoser/electron window on startup, disabling this on headless servers is recommended.
- `server.webUIInterface` which web interface Suwayomi should launch on startup, options are `"browser"` and `"electron"`
- `server.electronPath` path of the main electron executable, should be in double quotes
- `server.webUIFlavor` set `"WebUI"` to make the server download and update Suwayomi-WebUI automatically or `"Custom"` if you want the server to serve a custom web interface that you manage by yourself.
- Note: "Custom" would be useful if you want to test preview versions of Suwayomi-WebUI or when you are using or developing other web interfaces like the web version of Suwayomi-Sorayomi.
- `server.webUIChannel` allows to choose which update channel to use (only valid when flavor is set to "WebUI"). Use `"BUNDLED"` to use the version included in the server download, `"STABLE"` to use the latest stable release or `"PREVIEW"` to use the latest preview release (potentially buggy).
- `server.webUIUpdateCheckInterval` the interval time in hours at which to check for updates. Use `0` to disable update checking.
- `server.webUISubpath` controls on which sub-path the UI is served; by default, it will be accessible on `/` (i.e. directly), with this setting it can also be set to appear at e.g. `/suwayomi`
### Downloader
```
server.downloadAsCbz = true
server.downloadsPath = ""
server.autoDownloadNewChapters = false
server.excludeEntryWithUnreadChapters = true
server.autoDownloadNewChaptersLimit = 0
server.autoDownloadIgnoreReUploads = false
server.downloadConversions = {}
```
- `server.downloadAsCbz = true` configures Suwayomi to automatically compress chapters into CBZ.
- `server.downloadsPath = ""` the path where manga downloads will be stored, if the value is empty, the default directory `downloads` inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory) will be used. If you are on Windows the slashes `\` needs to be doubled(`\\`) or replaced with `/`
- `server.autoDownloadNewChapters = false` controls if Suwayomi should automatically download new chapters after a library update.
- `server.excludeEntryWithUnreadChapters = true` controls if Suwayomi will download new chapters for titles with unread chapters (requires `server.autoDownloadNewChapters`).
- `server.autoDownloadNewChaptersLimit = 0` sets how many chapters should be downloaded at most, `0` to disable the limit; if the limit is reached, new chapters will not be downloaded (requires `server.autoDownloadNewChapters`).
- `server.autoDownloadIgnoreReUploads = false` controls if Suwayomi will re-download re-uploads on update (requires `server.autoDownloadNewChapters`).
- `server.downloadConversions = {}` configures optional image conversions for all downloads. This is an [JSON object](https://en.wikipedia.org/wiki/JSON#Syntax), with the source image [mime type](https://en.wikipedia.org/wiki/Media_type) as the key and an object with the target mime type or url and options as value.
The following options are all valid:
```
server.downloadConversions = { "image/webp" : { target : "image/jpeg", compressionLevel = 0.8 }}
# -- or --
server.downloadConversions."image/webp" = {
target = "image/jpeg" # image type to convert to
compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
}
# -- a url example --
server.downloadConversions = { "default" : { target : "http://localhost:9999/convert" }}
# -- a url with all parameters example --
server.downloadConversions = {
"default" : {
target : "http://localhost:9999/convert",
callTimeout : 10m,
connectTimeout : 10s,
headers : {
"authorization" : "MyPassword"
}
}
}
```
A source mime type `default` can be used as fallback to convert all images; a target mime type of `none` can be used to disable conversion for a particular format.
This is an example curl command for what Suwayomi-Server will send to the conversion url: `curl -X POST "http://localhost:9999/convert" -F "image=@cat.png;type=image/png"`
- `server.serveConversions = {}` configures optional image conversions before serving the image to the client. It follows the same format as `server.downloadConversions`.
### Updater
```
server.excludeUnreadChapters = true
server.excludeNotStarted = true
server.excludeCompleted = true
server.globalUpdateInterval = 12
server.updateMangas = false
```
- `server.excludeUnreadChapters = true` controls if Suwayomi should include titles with unread chapters in the library update.
- `server.excludeNotStarted = true` controls if Suwayomi should include titles which weren't started yet in the library update.
- `server.excludeCompleted = true` controls if Suwayomi should include titles which are marked completed in the library update.
- `server.globalUpdateInterval = 12` sets the time in hours for the automatic library internal, `0` to disable it. Range: 6 <= n < ∞
- `server.updateMangas = false` controls if Suwayomi should also update title metadata along with fetching new chapters in the library update.
### Authentication
```
server.authMode = "none" # none, basic_auth, simple_login or ui_login
server.authUsername = "user"
server.authPassword = "pass"
server.jwtAudience = "suwayomi-server-api"
server.jwtTokenExpiry = "5m"
server.jwtRefreshExpiry = "60d"
```
- `server.authMode = "none"`: Since v2.1.1867, Suwayomi supports two modes of authentication. Enabling authentication is useful when hosting on a public network/the Internet. If you used the original `server.basicAuth*` variables, it will be automatically migrated.
- `basic_auth` configures Suwayomi for [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
- `simple_login` works similarly to Basic Authentication, but presents a custom login page. The login is stored via a cookie and needs to be refreshed on every server restart or every 30 minutes.
- Starting with v2.1.1894, another mode is available:
`ui_login` is a new [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token)-based authentication scheme, which tightly integrates with the chosen UI. Instead of restricting access completely, this allows the user to still open the UI (e.g. WebUI). Unlike the other two modes, this means the login page is entirely customizable by the UI. The `jwt*` settings can be used to tune session duration in this mode.
- `server.authUsername` the username value that you have to provide when authenticating.
- `server.authPassword` the password value that you have to provide when authenticating.
- `server.jwtAudience` is any string to be embedded into the JWT token. See `aud` field in [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token#Standard_fields).
- `server.jwtTokenExpiry` is the time any token is valid ("access token"); after expiry, the refresh token will be used to generate a new access token again without prompting for credentials.
- `server.jwtRefreshExpiry` is the time the refresh token is valid. After it has expired, the user will be prompted to login again; before that, the session can be quietly refreshed using this token. Note the security implication in this: While the access token is valid, the server fully trusts the holder of that token. After the access token has expired, the refresh token can be used to obtain a new access token; only at this time can the session be terminated by the server.
**Note**: Basic access authentication sends username and password in cleartext and is not completely secure over HTTP, it's recommended to pair this feature with a reverse proxy server like nginx and expose the server over HTTPS. Similarly, with `simple_login` and `ui_login`, the credentials are sent in cleartext on login, and the cookie/token is sent with every request. Also your browser caches the credentials/cookie, so you should be careful when accessing the server from non-private devices and use incognito mode.
### misc
```
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.maxLogFiles = 31
server.maxLogFileSize = "10mb"
server.maxLogFolderSize = "100mb"
server.extensionRepos = []
server.maxSourcesInParallel = 6
```
- `server.debugLogsEnabled` controls whether if Suwayomi-Server should print more information while being run inside a Terminal/CMD/Powershell window.
- `server.systemTrayEnabled = true` whether if Suwayomi-Server should show a System Tray Icon, disabling this on headless servers is recommended.
- `server.maxLogFiles = 31` sets the maximum number of days to keep files before they get deleted.
- `server.maxLogFileSize = "10mb"` sets the maximum size of a log file - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
- `server.maxLogFolderSize = "100mb"` sets the maximum size of all saved log files - values are formatted like: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
- `server.extensionRepos` is a list of extension repositories for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON file representing the repository.
- `server.maxSourcesInParallel = 6` sets how many sources can do requests (updates, downloads) in parallel. Updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously. Range: 1 <= n <= 20.
### Backup
```
server.backupPath = ""
server.backupTime = "00:00"
server.backupInterval = 1
server.backupTTL = 14
server.autoBackupIncludeManga = true
server.autoBackupIncludeCategories = true
server.autoBackupIncludeChapters = true
server.autoBackupIncludeTracking = true
server.autoBackupIncludeHistory = true
server.autoBackupIncludeClientData = true
server.autoBackupIncludeServerSettings = true
```
- `server.backupPath = ""` the path where backups will be stored, if the value is empty, the default directory `backups` inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory) will be used. If you are on Windows the slashes `\` needs to be doubled(`\\`) or replaced with `/`
- `server.backupTime = "00:00"` sets the time of day at which the automated backup should be triggered.
- `server.backupInterval = 1` sets the interval in which the server will automatically create a backup in days, `0` to disable it.
- `server.backupTTL = 14` sets how long backup files will be kept before they will get deleted in days, `0` to disable it.
- `server.autoBackupIncludeManga` whether to include manga data in automatic backups
- `server.autoBackupIncludeCategories` whether to include category data in automatic backups
- `server.autoBackupIncludeChapters` whether to include manga chapter data in automatic backups
- `server.autoBackupIncludeTracking` whether to include manga tracking data in automatic backups
- `server.autoBackupIncludeHistory` whether to include manga reading history in automatic backups
- `server.autoBackupIncludeClientData` whether to include client data in automatic backups
- `server.autoBackupIncludeServerSettings` whether to include server settings in automatic backups
### Local Source
```
server.localSourcePath = ""
```
- `server.localSourcePath = ""` the path from where local manga are loaded, if the value is empty, the default directory `local` inside [the data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory) will be used. If you are on Windows the slashes `\` needs to be doubled(`\\`) or replaced with `/`
### Cloudflare bypass
```
server.flareSolverrEnabled = false
server.flareSolverrUrl = "http://localhost:8191"
server.flareSolverrTimeout = 60 # time in seconds
server.flareSolverrSessionName = "suwayomi"
server.flareSolverrSessionTtl = 15 # time in minutes
server.flareSolverrAsResponseFallback = false
```
- `server.flareSolverrEnabled = false` controls if Suwayomi attempts to connect to FlareSolverr if a CloudFlare challenge is detected.
- `server.flareSolverrUrl = "http://localhost:8191"` sets the address where Suwayomi attempts to connect to FlareSolverr. The instance needs to run and be accessible from the server where Suwayomi is running for CloudFlare bypass to work.
- `server.flareSolverrTimeout = 60` sets the timeout to wait for FlareSolverr to solve a given challenge in seconds.
- `server.flareSolverrSessionName = "suwayomi"` sets the session name that FlareSolverr should use. Tells FlareSolverr which set of cookies to use.
- `server.flareSolverrSessionTtl = 15` sets the time for how long sessions in FlareSolverr should live in minutes. After this time, FlareSolverr will forget cookies.
- `server.flareSolverrAsResponseFallback = false` allows Suwayomi to use the contents of the request that FlareSolverr received in case Suwayomi sees a CloudFlare challenge but FlareSolverr does not (which prevents it from solving the challenge).
**Note:** Byparr is a popular alternative to FlareSolverr. It uses the same API schema as FlareSolverr, so you can also configure the address of a Byparr instance in `server.flareSolverrUrl`.
**Note:** The example [docker-compose.yml file](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/docker-compose.yml) contains everything you need to get started with Suwayomi+Byparr.
### OPDS
```
server.opdsUseBinaryFileSizes = false
server.opdsItemsPerPage = 50
server.opdsEnablePageReadProgress = true
server.opdsMarkAsReadOnDownload = false
server.opdsShowOnlyUnreadChapters = false
server.opdsShowOnlyDownloadedChapters = false
server.opdsChapterSortOrder = "DESC"
server.opdsCbzMimetype = "MODERN"
```
- `server.opdsUseBinaryFileSizes = false` controls if Suwayomi should display file sizes in binary units (KiB, MiB, GiB) or decimal (KB, MB, GB) in OPDS listings.
- `server.opdsItemsPerPage = 50` sets the number of items per page in OPDS listings. Range: 10 <= n <= 5000.
- `server.opdsEnablePageReadProgress = true` controls if Suwayomi should include information to track chapter read progress in OPDS chapter page listings. This will cause the reader to mark the pages as read when it downloads the images.
- `server.opdsMarkAsReadOnDownload = false` controls if Suwayomi should mark the chapters as read when it is downloaded through the OPDS listing.
- `server.opdsShowOnlyUnreadChapters = false` controls if OPDS listings should only include unread chapters.
- `server.opdsShowOnlyDownloadedChapters = false` controls if OPDS listings should only include downloaded chapters.
- `server.opdsChapterSortOrder = "DESC"` sets the default chapter sort order in OPDS listings, either `"ASC"` or `"DESC"`
- `server.opdsCbzMimetype = "MODERN"` controls which mimetype to use for CBZ downloads. This affects the offered link in OPDS, as well as the content type of the CBZ download. Allowed is MODERN (current IANA standard), LEGACY (deprecated mimetype for .cbz) and COMPATIBLE (deprecated mimetype for all comic archives). Use LEGACY or COMPATIBLE if older clients don't offer the chapter download (note that the chapter needs to first be downloaded in Suwayomi, before it is available in OPDS).
### KOReader Sync
```
server.koreaderSyncServerUrl = "https://sync.koreader.rocks/"
server.koreaderSyncUsername = ""
server.koreaderSyncUserkey = ""
server.koreaderSyncDeviceId = ""
server.koreaderSyncChecksumMethod = BINARY # BINARY or FILENAME
server.koreaderSyncPercentageTolerance = 1.0E-15 # range: [1.0E-15, 1.0]
server.koreaderSyncStrategyForward = PROMPT # PROMPT, KEEP_LOCAL, KEEP_REMOTE, DISABLED
server.koreaderSyncStrategyBackward = DISABLED # PROMPT, KEEP_LOCAL, KEEP_REMOTE, DISABLED
```
- `server.koreaderSyncServerUrl` where KOReader Sync Server is running. Public servers (e.g., `https://sync.koreader.rocks/`, `https://kosync.ak-team.com:3042/`) or self-hosted instances can also be used.
- `server.koreaderSyncUsername` the username with which to authenticate at the KOReader instance.
- `server.koreaderSyncUserkey` the password/key with which to authenticate at the KOReader instance.
- `server.koreaderSyncDeviceId` a unique ID to identify Suwayomi at the KOReader Sync Server. Leave blank to auto-generate.
- `server.koreaderSyncChecksumMethod` the method by which to identify chapters at the KOReader Sync Server. BINARY includes the entire contents of the chapter, and is thus more expensive, but may be convenient to catch updated chapters.
- `server.koreaderSyncPercentageTolerance` when syncing read progress for a chapter from other devices, how much difference (absolute) is allowed to be ignored. When above this tolerance, Suwayomi's read progress will be replaced by the remote device's according to the specified strategy. The strategy is chosed from `server.koreaderSyncStrategyForward` and `server.koreaderSyncStrategyBackward` based on the timestamps of the last read.
- `server.koreaderSyncStrategyForward` the strategy to apply when remote progress is newer than local.
- `server.koreaderSyncStrategyBackward` the strategy to apply when remote progress is older than local.
### Database
```
server.databaseType = H2 # H2, POSTGRESQL
server.databaseUrl = "postgresql://localhost:5432/suwayomi"
server.databaseUsername = ""
server.databasePassword = ""
server.useHikariConnectionPool = true
```
- `server.databaseType` chooses which type of database to use. [H2](https://en.wikipedia.org/wiki/H2_Database_Engine) is the default; it is a simple file-based database for Java applications. Since it is only based on files without a server process, file corruption can be common when the server is not shut down properly. [PostgreSQL](https://en.wikipedia.org/wiki/PostgreSQL) is a popular cross-platform, stable database. To use PostgreSQL, you need to run an instance yourself.
- `server.databaseUrl` the URL where to find the PostgreSQL server, including the database name.
- `server.databaseUsername` the username with which to authenticate at the PostgreSQL instance.
- `server.databasePassword` the username with which to authenticate at the PostgreSQL instance.
- `server.useHikariConnectionPool` use Hikari Connection Pool to connect to the database.
**Note:** The example [docker-compose.yml file](https://github.com/Suwayomi/Suwayomi-Server-docker/blob/main/docker-compose.yml) contains everything you need to get started with Suwayomi+PostgreSQL. Please be aware that PostgreSQL support is currently still in beta.
**Note:** These settings are excluded from backups, so a backup can be used to easily switch database installations by setting up the connection first, then restoring the backup.
## Overriding configuration options with command-line arguments
You can override the above configuration options with command-line arguments.
You usually only need to set this when using custom setups like a portable version of Suwayomi or your if your User dir cannot be written to or your system administrator doesn't allow it.
Use the pattern bellow.
```
java -Dsuwayomi.tachidesk.config.<configuration option 1>=<configuration value 1> -Dsuwayomi.tachidesk.config.<configuration option 2>=<configuration value 2> ... -Dsuwayomi.tachidesk.config.<configuration option N>=<configuration value N> -jar <path to server jar>
```
for example to force launching Suwayomi-Server in electron you would have something like:
```
java -Dsuwayomi.tachidesk.config.server.webUIInterface=electron -Dsuwayomi.tachidesk.config.server.electronPath="/path/to/electron" -jar Suwayomi-Server-v0.X.Y-rXXXX.jar
```
**Note:** you can put the command above in a custom launcher script like the ones found [here](https://github.com/Suwayomi/Suwayomi-Server/tree/master/scripts/resources).
## Overriding Suwayomi-Server's Data Directory path
The only possible way to override the default Data Directory path is with command-line arguments(the chicken and egg problem!).
Add the special config option `server.rootDir="<path to data directory>"` to your command-line arguments.
For example:
```
java -Dsuwayomi.tachidesk.config.server.rootDir="/path/to/data/directory" -jar Suwayomi-Server-v0.X.Y-rXXXX.jar
```
+8
View File
@@ -0,0 +1,8 @@
1. **Install the latest version(or preview):** https://github.com/Suwayomi/Suwayomi-Server/releases/latest
2. Find an extensions repo, there is now no default extensions, and you have to use Google to find a Tachiyomi extension repo.
- Note: The repo should look like `https://raw.githubusercontent.com/user/reponame` or `https://www.github.com/user/reponame`
3. Configure it using one of the following:
- Suwayomi-Server option 1: With the **new launcher**, go to the `Extensions` tab and add the extensions repo.
- Suwayomi-Server option 2: Use the **server settings** in **WebUI** to add the extensions repo.
- Suwayomi-Server option 3: Go to the `server.conf` file in the data files and add the extensions repo.
- Suwayomi docker container: Edit the `EXTENSION_REPOS` environment variable and add the extension repo in the format listed in the container README.
+3
View File
@@ -0,0 +1,3 @@
Welcome to the Suwayomi wiki!
Look at the pages section for links and stuff.
+135
View File
@@ -0,0 +1,135 @@
Follow the steps below to create local manga.
1. Place correctly structured manga inside the `local` directory located in [Suwayomi's data directory](https://github.com/Suwayomi/Suwayomi-Server/wiki/The-Data-Directory).
1. You can then access the manga in the **Local source** source.
If you add more chapters then you'll have to manually refresh the chapter list.
Supported chapter formats are folder with pictures inside (such as `.jpg`, `.png`, etc.), `ZIP`/`CBZ`, `RAR`/`CBR` and `EPUB`. But expect better performance with directories and `ZIP`/`CBZ`.
**Note:** While Suwayomi does support chapters compressed as **RAR** or **CBR**, note that **RAR** or **CBR** files using the **RAR5** format are not supported yet.
**Note:** If **CBR** or **RAR** files do not work, you may need to extract and re-compress them into one of the supported formats.
## Folder Structure
Suwayomi requires a specific folder structure for local manga to be correctly processed. Local manga will be read from the `local` folder. Each manga must have a `Manga` folder and a `Chapter` folder. Images will then go into the chapter folder. See below for more information on archive files. You can refer to the following example:
<div class="side-by-side">
<ul class="file-tree">
<li>
local/
<ul>
<li>
<span class="ft-icon ft-folder">Manga title/</span>
<ul>
<li>
<span class="ft-icon ft-folder">Chapter 01 - Prologue/</span>
<ul>
<li><span class="ft-icon ft-image">page01.png</span></li>
<li><span class="ft-icon ft-image">page02.png</span></li>
<li><span class="ft-icon ft-image">page03.png</span></li>
<li><span class="ft-icon ft-image">...</span></li>
</ul>
</li>
<li>
<span class="ft-icon ft-folder">Chapter 02 - A hero's journey/</span>
<ul>
<li><span class="ft-icon ft-image">01.jpg</span></li>
<li><span class="ft-icon ft-image">02.jpg</span></li>
<li><span class="ft-icon ft-image">03.jpg</span></li>
<li><span class="ft-icon ft-image">...</span></li>
</ul>
</li>
<li><span class="ft-icon ft-image">cover.jpg</span></li>
</ul>
</li>
<li>
<span class="ft-icon ft-folder">Other Manga title/</span>
<ul>
<li><span class="ft-icon ft-image">cover.jpg</span></li>
<li>
<span class="ft-icon ft-folder">ch001/</span>
<ul>
<li><span class="ft-icon ft-image">01 - cover.jpg</span></li>
<li><span class="ft-icon ft-image">02 - first page.jpeg</span></li>
<li><span class="ft-icon ft-image">03 - second.jpg</span></li>
<li><span class="ft-icon ft-image">...</span></li>
</ul>
</li>
<li>
<span class="ft-icon ft-folder">ch002/</span>
<ul>
<li><span class="ft-icon ft-image">001.jpg</span></li>
<li><span class="ft-icon ft-image">002.jpg</span></li>
<li><span class="ft-icon ft-image">003.jpg</span></li>
<li><span class="ft-icon ft-image">...</span></li>
</ul>
</li>
</ul>
</li>
<li>...</li>
</ul>
</li>
</ul>
</div>
**Note:** Chapter and page names should be ordered alphanumerically, using zero padded name prefixes is usually the best option.
The path to the folder with images must contain both the manga title and the chapter name (as seen above).
## Archive Files
Archive files such as `ZIP`/`CBZ` are supported but the folder structure inside is not. Any folders inside the archive file are ignored. You must place the archive inside the `Manga` folder where the name will become the `Chapter` title. All images inside the archive regardless of folder structure will become pages for that chapter.
<ul class="file-tree">
<li>
local/
<ul>
<li>
<span class="ft-icon ft-folder">Manga title/</span>
<ul>
<li>
<span class="ft-icon ft-zip">ch1.zip</span>
</li>
<li>
<span class="ft-icon ft-zip">ch2.zip</span>
</li>
<span class="ft-icon ft-image">cover.jpg</span>
</ul>
</li>
<li>...</li>
</ul>
</li>
</ul>
## Advanced
### Editing local manga details
It is possible to add details to local manga. Like manga from other catalogs, you add information about the manga such as the author, artist, description, and genre tags.
To import details along with your local manga, you have to create a json file. It can be named anything but it must be placed within the **Manga** folder. A standard file name is `details.json`. This file will contain the extended details about the manga in the `JSON` format. You can see the example below on how to build the file. Once the file is there, the app should load the data when you first open the manga, or you can pull down to refresh the details.
You can copy the following example and edit the details as needed:
``` json
{
"title": "Example Title",
"author": "Example Author",
"artist": "Example Artist",
"description": "Example Description",
"genre": ["genre 1", "genre 2", "etc"],
"status": "0",
"_status values": ["0 = Unknown", "1 = Ongoing", "2 = Completed", "3 = Licensed"]
}
```
### Using a custom cover image
It is also possible to use a custom image as a cover for each local manga.
To do this, you only need to place the image file, that needs to be named
`cover.jpg`, in the root of the manga folder. The app will then use your
custom image in the local source listing.
+17
View File
@@ -0,0 +1,17 @@
## Default data directory
The default data directory is located below depending on your operating system:
Replace `<Account>` with your account/username.
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Custom
You can set Suwayomi-Server to use a specific directory with the `-Dsuwayomi.tachidesk.config.server.rootDir` startup argument.
An example of this is `java -Dsuwayomi.tachidesk.config.server.rootDir="D:\Tachidesk Data" -jar Tachidesk-vX.Y.Z-rxxxx.jar`
+19
View File
@@ -0,0 +1,19 @@
## General Troubleshooting
This guide will try to fix Suwayomi by reseting it to a clean installation state.
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
- Clear all browsing data on your browser if you use Suwayomi from a browser.
- Delete the Suwayomi data directory located below and re-run the app.
Note: Replace `<Account>` with the currently logged in account/username on your pc.
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
+69 -41
View File
@@ -1,19 +1,22 @@
[versions] [versions]
kotlin = "2.1.20" kotlin = "2.3.21"
coroutines = "1.10.1" coroutines = "1.11.0"
serialization = "1.8.0" serialization = "1.11.0"
okhttp = "5.0.0-alpha.14" # Major version is locked by Tachiyomi extensions jvmTarget = "21"
javalin = "6.5.0" okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` javalin = "7.2.0"
exposed = "0.59.0" jte = "3.2.4"
dex2jar = "v64" # Stuck until https://github.com/ThexXTURBOXx/dex2jar/issues/27 is fixed jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
polyglot = "24.2.0" exposed = "0.61.0"
dex2jar = "2.4.36"
polyglot = "25.0.3"
settings = "1.3.0" settings = "1.3.0"
twelvemonkeys = "3.12.0" twelvemonkeys = "3.13.1"
graphqlkotlin = "8.4.0" graphqlkotlin = "8.9.0"
xmlserialization = "0.90.3" xmlserialization = "0.91.3"
ktlint = "1.5.0" ktlint = "1.8.0"
koin = "4.0.2" koin = "4.2.1"
moko = "0.26.4"
[libraries] [libraries]
# Kotlin # Kotlin
@@ -35,27 +38,29 @@ serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", v
# Logging # Logging
slf4japi = "org.slf4j:slf4j-api:2.0.17" slf4japi = "org.slf4j:slf4j-api:2.0.17"
logback = "ch.qos.logback:logback-classic:1.5.18" logback = "ch.qos.logback:logback-classic:1.5.32"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.5" kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
# OkHttp # OkHttp
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" } okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
okio = "com.squareup.okio:okio:3.10.2" okio = "com.squareup.okio:okio:3.17.0"
# Javalin api # Javalin api
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" } javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" } javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
jte = { module = "gg.jte:jte", version.ref = "jte" }
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
# GraphQL # GraphQL
graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" } graphql-kotlin-server = { module = "com.expediagroup:graphql-kotlin-server", version.ref = "graphqlkotlin" }
graphql-kotlin-scheme = { module = "com.expediagroup:graphql-kotlin-schema-generator", version.ref = "graphqlkotlin" } graphql-kotlin-scheme = { module = "com.expediagroup:graphql-kotlin-schema-generator", version.ref = "graphqlkotlin" }
graphql-java-core = "com.graphql-java:graphql-java:22.3" # Major version locked by graphql-kotlin
graphql-java-scalars = "com.graphql-java:graphql-java-extended-scalars:22.0" graphql-java-scalars = "com.graphql-java:graphql-java-extended-scalars:22.0"
# Exposed ORM # Exposed ORM
@@ -63,10 +68,12 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
postgres = "org.postgresql:postgresql:42.7.11"
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
hikaricp = "com.zaxxer:HikariCP:7.0.2"
# Exposed Migrations # Exposed Migrations
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.7.0" exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.8.0"
# Dependency Injection # Dependency Injection
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
@@ -79,10 +86,10 @@ systemtray-desktop = "com.dorkbox:Desktop:1.1" # version locked by SystemTray
# dependencies of Tachiyomi extensions # dependencies of Tachiyomi extensions
injekt = "com.github.null2264:injekt-koin:ee267b2e27" injekt = "com.github.null2264:injekt-koin:ee267b2e27"
rxjava = "io.reactivex:rxjava:1.3.8" rxjava = "io.reactivex:rxjava:1.3.8"
jsoup = "org.jsoup:jsoup:1.19.1" jsoup = "org.jsoup:jsoup:1.22.2"
# Config # Config
config = "com.typesafe:config:1.4.3" config = "com.typesafe:config:1.4.8"
config4k = "io.github.config4k:config4k:0.7.0" config4k = "io.github.config4k:config4k:0.7.0"
# Sort # Sort
@@ -92,29 +99,29 @@ sort = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
android-stubs = "com.github.Suwayomi:android-jar:1.0.0" android-stubs = "com.github.Suwayomi:android-jar:1.0.0"
# Asm modificiation # Asm modificiation
asm = "org.ow2.asm:asm:9.5" # version locked by Dex2Jar asm = "org.ow2.asm:asm:9.9.1" # version locked by Dex2Jar
dex2jar-translator = { module = "com.github.ThexXTURBOXx.dex2jar:dex-translator", version.ref = "dex2jar" } dex2jar-translator = { module = "de.femtopedia.dex2jar:dex-translator", version.ref = "dex2jar" }
dex2jar-tools = { module = "com.github.ThexXTURBOXx.dex2jar:dex-tools", version.ref = "dex2jar" } dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex2jar" }
# APK # APK
apk-parser = "net.dongliu:apk-parser:2.6.10" apk-parser = "net.dongliu:apk-parser:2.6.10"
apksig = "com.android.tools.build:apksig:8.9.0" apksig = "com.android.tools.build:apksig:9.2.1"
# Xml # Xml
xmlpull = "xmlpull:xmlpull:1.1.3.4a" xmlpull = "xmlpull:xmlpull:1.1.3.4a"
# Disk & File # Disk & File
appdirs = "net.harawata:appdirs:1.4.0" appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0" cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5" zip4j = "net.lingala.zip4j:zip4j:2.11.6"
commonscompress = "org.apache.commons:commons-compress:1.27.1" commonscompress = "org.apache.commons:commons-compress:1.28.0"
junrar = "com.github.junrar:junrar:7.5.5" junrar = "com.github.junrar:junrar:7.5.10"
# AES/CBC/PKCS7Padding Cypher provider # AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.80" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
# AndroidX annotations # AndroidX annotations
android-annotations = "androidx.annotation:annotation:1.9.1" android-annotations = "androidx.annotation:annotation:1.10.0"
# Substitute for duktape-android # Substitute for duktape-android
polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" } polyglot-core = { module = "org.graalvm.polyglot:polyglot", version.ref = "polyglot" }
@@ -125,7 +132,7 @@ settings-core = { module = "com.russhwolf:multiplatform-settings-jvm", version.r
settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" } settings-serialization = { module = "com.russhwolf:multiplatform-settings-serialization-jvm", version.ref = "settings" }
# ICU4J # ICU4J
icu4j = "com.ibm.icu:icu4j:77.1" icu4j = "com.ibm.icu:icu4j:78.3"
# Image Decoding implementation provider # Image Decoding implementation provider
twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" } twelvemonkeys-common-lang = { module = "com.twelvemonkeys.common:common-lang", version.ref = "twelvemonkeys" }
@@ -136,8 +143,10 @@ twelvemonkeys-imageio-metadata = { module = "com.twelvemonkeys.imageio:imageio-m
twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" } twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" } twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
imageio-webp = "com.github.usefulness:webp-imageio:0.10.2"
# Testing # Testing
mockk = "io.mockk:mockk:1.13.17" mockk = "io.mockk:mockk:1.14.9"
# cron scheduler # cron scheduler
cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5" cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
@@ -145,25 +154,41 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5"
# cron-utils # cron-utils
cronUtils = "com.cronutils:cron-utils:9.2.1" cronUtils = "com.cronutils:cron-utils:9.2.1"
# Webview
kcef = "dev.datlag:kcef:2024.04.20.4"
# User
jwt = "com.auth0:java-jwt:4.5.2"
# lint - used for renovate to update ktlint version # lint - used for renovate to update ktlint version
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" } ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
# moko
moko = { module = "dev.icerock.moko:resources", version.ref = "moko" }
[plugins] [plugins]
# Kotlin # Kotlin
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"}
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin"}
# Linter # Linter
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.2.0"} ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.2.0"}
# Build config # Build config
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.4"} buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.9"}
# Download # Download
download = { id = "de.undercouch.download", version = "5.6.0"} download = { id = "de.undercouch.download", version = "5.7.0"}
# ShadowJar # ShadowJar
shadowjar = { id = "com.github.johnrengelman.shadow", version = "8.1.1"} shadowjar = { id = "com.gradleup.shadow", version = "8.3.10"}
# Moko
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
# JTE
jte = { id = "gg.jte.gradle", version.ref = "jte" }
[bundles] [bundles]
shared = [ shared = [
@@ -186,7 +211,8 @@ shared = [
"dex2jar-translator", "dex2jar-translator",
"dex2jar-tools", "dex2jar-tools",
"apk-parser", "apk-parser",
"jackson-annotations" "jackson-annotations",
"kcef"
] ]
sharedTest = [ sharedTest = [
@@ -203,6 +229,8 @@ okhttp = [
javalin = [ javalin = [
"javalin-core", "javalin-core",
#"javalin-openapi", #"javalin-openapi",
"javalin-rendering",
"jte",
] ]
jackson = [ jackson = [
"jackson-databind", "jackson-databind",
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+3 -6
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+10 -22
View File
@@ -23,8 +23,8 @@
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+14 -6
View File
@@ -7,15 +7,23 @@
"customManagers": [ "customManagers": [
{ {
"customType": "regex", "customType": "regex",
"fileMatch": [ "managerFilePatterns": [
"scripts/bundler.sh" "/scripts/bundler.sh/"
], ],
"matchStrings": [ "matchStrings": [
"JRE_RELEASE=[\"'](?<currentValue>.+?)[\"']\\s+" "JRE_ZULU=[\"'](?<currentValue>.+?)[\"']"
], ],
"datasourceTemplate": "github-releases", "depNameTemplate": "zulu",
"depNameTemplate": "adoptium/temurin21-binaries", "datasourceTemplate": "custom.zulu",
"versioningTemplate": "regex:^jdk-?(?<major>\\d+).(?<minor>\\d+).+?(?<patch>[\\d+]+)$" "versioningTemplate": "regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+).*$"
} }
],
"customDatasources": {
"zulu": {
"defaultRegistryUrlTemplate": "https://api.azul.com/metadata/v1/zulu/packages?availability_types=ca&release_status=both&java_package_type=jre&crac_supported=false&javafx_bundled=false&support_term=lts&arch=x86&os=linux&archive_type=zip&page_size=1000&include_fields=java_package_features,release_status,support_term,os,arch,hw_bitness,abi,java_package_type,javafx_bundled,sha256_hash,cpu_gen,size,archive_type,certifications,lib_c_type,crac_supported&page=1&azul_com=true",
"transformTemplates": [
"{\"releases\": $$.$join([$join($map(distro_version[[0..2]], $string), \".\"), \"_\", $join($map(java_version, $string), \".\")])}"
] ]
} }
}
}
+81 -24
View File
@@ -30,7 +30,7 @@ main() {
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS" RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+") RELEASE_VERSION=$(echo "$JAR" | grep -oP "v\K[0-9]+\.[0-9]+\.[0-9]+")
#RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)" #RELEASE_REVISION_NUMBER="$(tmp="${JAR%.*}" && echo "${tmp##*-}" | tr -d r)"
local electron_version="v28.1.3" local electron_version="v37.2.5"
# clean temporary directory on function return # clean temporary directory on function return
trap "rm -rf $RELEASE_NAME/" RETURN trap "rm -rf $RELEASE_NAME/" RETURN
@@ -38,12 +38,32 @@ main() {
download_launcher download_launcher
if [ ! -f scripts/resources/catch_abort.so ]; then
gcc -fPIC -shared scripts/resources/catch_abort.c -lpthread -o scripts/resources/catch_abort.so
fi
JRE_ZULU="25.30.17_25.0.1"
JRE_RELEASE="jre${JRE_ZULU#*_}" # e.g. jre25.0.1
ZULU_RELEASE="zulu${JRE_ZULU%_*}" # e.g. zulu25.30.17
case "$OS" in case "$OS" in
debian-all) debian-all)
RELEASE="$RELEASE_NAME.deb" RELEASE="$RELEASE_NAME.deb"
download_jogamp "linux-*" # it's easier to bundle them ourselves than to handle Debian's path conventions
make_deb_package make_deb_package
move_release_to_output_dir move_release_to_output_dir
;; ;;
appimage)
JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-linux_x64.zip"
JRE_DIR="${JRE%.*}"
JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
download_jogamp "linux-amd64"
setup_jre
RELEASE="$RELEASE_NAME.AppImage"
make_appimage
move_release_to_output_dir
;;
linux-assets) linux-assets)
RELEASE="$RELEASE_NAME.tar.gz" RELEASE="$RELEASE_NAME.tar.gz"
copy_linux_package_assets_to "$RELEASE_NAME/" copy_linux_package_assets_to "$RELEASE_NAME/"
@@ -51,14 +71,13 @@ main() {
move_release_to_output_dir move_release_to_output_dir
;; ;;
linux-x64) linux-x64)
# https://github.com/adoptium/temurin21-binaries/releases/ JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-linux_x64.zip"
JRE_RELEASE="jdk-21.0.6+7" JRE_DIR="${JRE%.*}"
JRE="OpenJDK21U-jre_x64_linux_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz" JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-linux-x64.zip" ELECTRON="electron-$electron_version-linux-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_electron download_electron
download_jogamp "linux-amd64"
setup_jre setup_jre
tree "$RELEASE_NAME" tree "$RELEASE_NAME"
@@ -67,14 +86,13 @@ main() {
move_release_to_output_dir move_release_to_output_dir
;; ;;
macOS-x64) macOS-x64)
# https://github.com/adoptium/temurin21-binaries/releases/ JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-macosx_x64.zip"
JRE_RELEASE="jdk-21.0.6+7" JRE_DIR="${JRE%.*}"
JRE="OpenJDK21U-jre_x64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz" JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-x64.zip" ELECTRON="electron-$electron_version-darwin-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_electron download_electron
download_jogamp "macosx-universal"
setup_jre setup_jre
tree "$RELEASE_NAME" tree "$RELEASE_NAME"
@@ -83,14 +101,13 @@ main() {
move_release_to_output_dir move_release_to_output_dir
;; ;;
macOS-arm64) macOS-arm64)
# https://github.com/adoptium/temurin21-binaries/releases/ JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-macosx_aarch64.zip"
JRE_RELEASE="jdk-21.0.6+7" JRE_DIR="${JRE%.*}"
JRE="OpenJDK21U-jre_aarch64_mac_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').tar.gz" JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-darwin-arm64.zip" ELECTRON="electron-$electron_version-darwin-arm64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_electron download_electron
download_jogamp "macosx-universal"
setup_jre setup_jre
tree "$RELEASE_NAME" tree "$RELEASE_NAME"
@@ -99,14 +116,13 @@ main() {
move_release_to_output_dir move_release_to_output_dir
;; ;;
windows-x64) windows-x64)
# https://github.com/adoptium/temurin21-binaries/releases/ JRE="$ZULU_RELEASE-ca-$JRE_RELEASE-win_x64.zip"
JRE_RELEASE="jdk-21.0.6+7" JRE_DIR="${JRE%.*}"
JRE="OpenJDK21U-jre_x64_windows_hotspot_$(echo "$JRE_RELEASE" | sed 's/jdk//;s/-//g;s/+/_/g').zip" JRE_URL="https://cdn.azul.com/zulu/bin/$JRE"
JRE_DIR="$JRE_RELEASE-jre"
JRE_URL="https://github.com/adoptium/temurin21-binaries/releases/download/$JRE_RELEASE/$JRE"
ELECTRON="electron-$electron_version-win32-x64.zip" ELECTRON="electron-$electron_version-win32-x64.zip"
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON" ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_electron download_electron
download_jogamp "windows-amd64"
setup_jre setup_jre
tree "$RELEASE_NAME" tree "$RELEASE_NAME"
@@ -138,6 +154,18 @@ download_launcher() {
mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar" mv "Suwayomi-Launcher.jar" "$RELEASE_NAME/Suwayomi-Launcher.jar"
} }
download_jogamp() {
local platform="$1"
if [ ! -f jogamp-all-platforms.7z ]; then
curl "https://jogamp.org/deployment/jogamp-current/archive/jogamp-all-platforms.7z" -o jogamp-all-platforms.7z
fi
7z x jogamp-all-platforms.7z "jogamp-all-platforms/lib/$platform/"
mkdir -p "$RELEASE_NAME/natives/"
mv jogamp-all-platforms/lib/* "$RELEASE_NAME/natives/"
rm -rf jogamp-all-platforms
}
download_electron() { download_electron() {
if [ ! -f "$ELECTRON" ]; then if [ ! -f "$ELECTRON" ]; then
curl -L "$ELECTRON_URL" -o "$ELECTRON" curl -L "$ELECTRON_URL" -o "$ELECTRON"
@@ -184,6 +212,7 @@ make_linux_bundle() {
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar" cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/" cp "scripts/resources/suwayomi-launcher.sh" "$RELEASE_NAME/"
cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/" cp "scripts/resources/suwayomi-server.sh" "$RELEASE_NAME/"
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/bin/"
tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/" tar -I "gzip -9" -cvf "$RELEASE" "$RELEASE_NAME/"
} }
@@ -205,17 +234,21 @@ make_deb_package() {
local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz" local upstream_source="suwayomi-server_$RELEASE_VERSION.orig.tar.gz"
mkdir "$RELEASE_NAME/$source_dir/" mkdir "$RELEASE_NAME/$source_dir/"
mv "$RELEASE_NAME/natives" "$RELEASE_NAME/$source_dir/natives"
mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar" mv "$RELEASE_NAME/Suwayomi-Launcher.jar" "$RELEASE_NAME/$source_dir/Suwayomi-Launcher.jar"
cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar" cp "$JAR" "$RELEASE_NAME/$source_dir/Suwayomi-Server.jar"
copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/" copy_linux_package_assets_to "$RELEASE_NAME/$source_dir/"
cp "scripts/resources/catch_abort.so" "$RELEASE_NAME/$source_dir/"
tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir" tar -I "gzip" -C "$RELEASE_NAME/" -cvf "$upstream_source" "$source_dir"
cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/" cp -r "scripts/resources/deb/" "$RELEASE_NAME/$source_dir/debian/"
sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog" sed -i "s/\$pkgver/$RELEASE_VERSION/" "$RELEASE_NAME/$source_dir/debian/changelog"
sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog" sed -i "s/\$pkgrel/1/" "$RELEASE_NAME/$source_dir/debian/changelog"
if [ "${CI:-}" = true ]; then
sudo apt update sudo apt update
sudo apt install devscripts build-essential dh-exec sudo apt install devscripts build-essential dh-exec
fi
cd "$RELEASE_NAME/$source_dir/" cd "$RELEASE_NAME/$source_dir/"
dpkg-buildpackage --no-sign --build=all dpkg-buildpackage --no-sign --build=all
cd - cd -
@@ -224,13 +257,34 @@ make_deb_package() {
mv "$RELEASE_NAME/$deb" "$RELEASE" mv "$RELEASE_NAME/$deb" "$RELEASE"
} }
# https://linuxconfig.org/building-a-hello-world-appimage-on-linux
make_appimage() {
local APPIMAGE_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
local APPIMAGE_TOOLNAME="appimagetool-x86_64.AppImage"
mkdir "$RELEASE_NAME/bin/"
cp "$JAR" "$RELEASE_NAME/bin/Suwayomi-Server.jar"
cp "scripts/resources/pkg/suwayomi-server.desktop" "$RELEASE_NAME/suwayomi-server.desktop"
cp "server/src/main/resources/icon/faviconlogo.png" "$RELEASE_NAME/suwayomi-server.png"
cp "scripts/resources/appimage/AppRun" "$RELEASE_NAME/AppRun"
chmod +x "$RELEASE_NAME/AppRun"
if [ "${CI:-}" = true ]; then
sudo apt update
sudo apt install libfuse2
fi
curl -L $APPIMAGE_URL -o $APPIMAGE_TOOLNAME
chmod +x $APPIMAGE_TOOLNAME
ARCH=x86_64 ./$APPIMAGE_TOOLNAME "$RELEASE_NAME" "$RELEASE"
}
make_windows_bundle() { make_windows_bundle() {
## I disabled this section until someone find a solution to this error: ## I disabled this section until someone find a solution to this error:
##E: Unable to correct problems, you have held broken packages. ##E: Unable to correct problems, you have held broken packages.
##./bundler.sh: line 250: wine: command not found ##./bundler.sh: line 250: wine: command not found
## check if running under github actions ## check if running under github actions
#if [ "$CI" = true ]; then #if [ "${CI:-}" = true ]; then
## change electron executable's icon ## change electron executable's icon
#sudo dpkg --add-architecture i386 #sudo dpkg --add-architecture i386
#wget -qO - https://dl.winehq.org/wine-builds/winehq.key \ #wget -qO - https://dl.winehq.org/wine-builds/winehq.key \
@@ -261,7 +315,7 @@ make_windows_bundle() {
} }
make_windows_package() { make_windows_package() {
if [ "$CI" = true ]; then if [ "${CI:-}" = true ]; then
sudo apt update sudo apt update
sudo apt install -y wixl sudo apt install -y wixl
fi fi
@@ -272,6 +326,9 @@ make_windows_package() {
find "$RELEASE_NAME/electron" \ find "$RELEASE_NAME/electron" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \ | wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs" --directory-ref electron --component-group electron >"$RELEASE_NAME/electron.wxs"
find "$RELEASE_NAME/natives" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
--directory-ref natives --component-group natives >"$RELEASE_NAME/natives.wxs"
find "$RELEASE_NAME/bin" \ find "$RELEASE_NAME/bin" \
| wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \ | wixl-heat --var var.SourceDir -p "$RELEASE_NAME/" \
@@ -282,7 +339,7 @@ make_windows_package() {
wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \ wixl -D ProductVersion="$RELEASE_VERSION" -D SourceDir="$RELEASE_NAME" \
-D Icon="$icon" --arch "$arch" "scripts/resources/msi/suwayomi-server-$arch.wxs" \ -D Icon="$icon" --arch "$arch" "scripts/resources/msi/suwayomi-server-$arch.wxs" \
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE" "$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" "$RELEASE_NAME/natives.wxs" "$RELEASE_NAME/bin.wxs" -o "$RELEASE"
} }
# Error handler # Error handler
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# exit early in case the file already exists
if [ -f /home/suwayomi/.local/share/Tachidesk/server.conf ]; then
exit 0
fi
mkdir -p /home/suwayomi/.local/share/Tachidesk
# extract the server reference config from the jar
unzip -q -j /home/suwayomi/startup/tachidesk_latest.jar "server-reference.conf" -d /home/suwayomi/startup
# move and rename the reference config
mv /home/suwayomi/startup/server-reference.conf /home/suwayomi/.local/share/Tachidesk/server.conf
+83
View File
@@ -0,0 +1,83 @@
#!/bin/sh
set -e
url="$1"
arch="$2"
installdir="${3:-/opt/kcef/jcef}"
echo "Will try to download matching KCEF to $installdir, arch=$arch, api url=$url"
if [ -d "$installdir" ] && [ -f "$installdir/install.lock" ]; then
echo "JCEF already downloaded to $installdir, nothing to do"
exit 0
fi
if [ -z "$url" ]; then
echo "Not downloading KCEF since no URL specified"
exit 0
fi
arpath=/tmp/kcef/jcef.tar.gz
expath=/tmp/kcef/jcef
mkdir -p "$expath"
if [ ! -f "$arpath" ]; then
body="`curl -# -H 'accept: application/vnd.github+json' "$url" | jq -r '.body'`"
archive="`echo "$body" | gawk -F'|' '
function compare_sdk(i1, v1, i2, v2) {
# sort sdks last (https://github.com/DatL4g/KCEF/blob/0665269b7d6a91b0ee187f4432bb5be5ca41a112/kcef/src/main/kotlin/dev/datlag/kcef/KCEFBuilder.kt#L743-L755)
if (v1 ~ /sdk/ && v2 ~ /sdk/) return 0;
if (v1 ~ /sdk/) return 1;
if (v2 ~ /sdk/) return -1;
return 0;
}
BEGIN {
# ensure urls is an array
delete urls[0];
# parse os/arch tuple
match("'"$arch"'", /(.*)\/(.*)/, a);
os=a[1];
arch=a[2];
if (arch == "amd64") arch="x64";
if (arch == "arm64") arch="aarch64";
}
# for each line, check that the third table column contains a url and if so, extract it
# also need to check that it contains JCEF and matches the os/arch tuple
match($4, /(https?:\/\/|www.)[-a-zA-Z0-9+&@#\/%?=~_|!:.;]*[-a-zA-Z0-9+&@#/%=~_|]/, m) {
# if so, push to the urls array; there is no push function, so do this cursed construction
# arrays by convention start at 1, so do that
if (m[0] ~ /jcef/ && m[0] ~ os && m[0] ~ arch && m[0] ~ /\.tar\.gz$/) urls[length(urls)+1] = m[0];
}
END {
# now make sure sdk is sorted last, since we dont actually need the full sdk
asort(urls, sorted, "compare_sdk");
for (x in sorted) print sorted[x];
}
' | head -n1`"
if [ -z "$archive" ]; then
echo "No suitable archive found on release page, so not downloading"
exit 0
fi
echo "Found suitable JCEF release: $archive"
curl -# -L -H 'accept: application/x-tar' -o "$arpath" "$archive"
fi
set -xe
tar -C "$expath" -xf "$arpath"
libfolder="`find "$expath" -type d -name lib`"
if [ -z "$libfolder" ]; then
echo "lib folder not found in extracted archive, aborting"
rm -rf /tmp/kcef
exit 0
fi
mkdir -p "$installdir"
rmdir "$installdir" # we abuse -p to make sure all parent directories are created, then delete the actual target, since mv would move the libfolder inside otherwise
mv "$libfolder" "$installdir"
chmod -R a+x "$installdir"
touch "$installdir/install.lock"
rm -rf /tmp/kcef
+1 -1
View File
@@ -1 +1 @@
start "" jre/bin/javaw -jar Suwayomi-Launcher.jar start "" jre/bin/javaw --add-exports=java.desktop/sun.awt=ALL-UNNAMED -jar Suwayomi-Launcher.jar %*

Some files were not shown because too many files have changed in this diff Show More