Compare commits

..
438 Commits
Author SHA1 Message Date
txyyh e568e688f5 Release 1.8.4.0-alpha02
Releases / Build (push) Has been cancelled
2022-12-09 10:59:34 +00:00
FooIbar a201c930c2 Fix "Unable to load PublicSuffixDatabase.gz" 2022-12-09 18:27:03 +08:00
小白-白 7d927dde6b Resolve #505 2022-12-09 18:20:28 +08:00
tarsin f76fece81d Increase LruCache capability to 512 MB 2022-12-09 18:10:42 +08:00
tarsin 069d7e2382 Cleanup ImageDecoder Wrapper
We have removed all user of these
No logical changes
2022-12-09 18:08:26 +08:00
FooIbar e2e0562719 Resolve #501 2022-12-09 10:01:36 +08:00
FooIbar 347aaadb43 Resolve #500 2022-12-09 10:01:36 +08:00
renovate[bot] 6ddd64232f Update plugin com.google.devtools.ksp to v1.8.0-RC-1.0.8 (#503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-09 08:20:18 +08:00
renovate[bot] e532b3b05b Update dependency gradle to v8.0-milestone-5 2022-12-08 23:12:38 +00:00
小白-白 d7c17d80e7 Close db before export, Resolve #361 2022-12-08 15:40:22 +08:00
tarsin b35980cf60 Drop outdated workaround 2022-12-08 11:06:18 +08:00
tarsin 263853d6a7 Always collapse appbar when created, set CollapseMode to scale 2022-12-08 10:37:59 +08:00
tarsin 6a100b8250 Use CollapsingToolbar.Large, Resolve #482 2022-12-08 10:02:48 +08:00
renovate[bot] 893560e589 Update room_version to v2.5.0-rc01 (#498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 09:18:03 +08:00
renovate[bot] 6b7ce7de5b Update fragment_version to v1.6.0-alpha04 (#496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 09:06:20 +08:00
renovate[bot] 6082d8cc1a Update plugin org.jetbrains.kotlin.android to v1.8.0-RC (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 09:06:03 +08:00
renovate[bot] ec37c9cab2 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha05 (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-08 08:23:02 +08:00
txyyh 309e2a715e Release 1.8.4.0-alpha01
Releases / Build (push) Has been cancelled
2022-12-07 12:28:52 +00:00
tarsin fbb4afa4f7 Fix searchlayout style 2022-12-07 20:26:54 +08:00
tarsin 90863b95f4 Fix fab position 2022-12-07 20:18:56 +08:00
tarsin 3576eaf3ff Try to fix thumb load 2022-12-07 20:08:53 +08:00
tarsin f14c9741ea Increase card margin 2022-12-07 19:47:49 +08:00
tarsin 6d04e51e10 Fix amoled mode apply 2022-12-07 19:26:17 +08:00
tarsin 6ea1aa6c1a Fix searchbar 2022-12-07 18:51:44 +08:00
tarsin c86e1a9409 Drop unused widget 2022-12-07 18:00:50 +08:00
tarsin f1845f2d4e Use transparent card background 2022-12-07 14:41:59 +08:00
tarsin a20ca4ea2e Convert FavouriteScene to New SearchBar, Kill hippo's searchbar 2022-12-07 14:37:32 +08:00
tarsin 0effc0d181 Fix searchfab 2022-12-07 14:18:03 +08:00
tarsin 7ce273ec67 Initial migrate GalleryListScene to New SearchBar, #397 2022-12-07 13:47:10 +08:00
tarsin 39e080cdca Fix thumb flicker 2022-12-06 18:14:17 +08:00
tarsin 17584ae5fb Avoid double authenticating on Android 10 2022-12-06 12:01:58 +08:00
小白-白 c46c72d66d Add private category 2022-12-05 18:05:16 +08:00
小白-白 36f00ab568 Fix disowned parsing 2022-12-05 18:05:16 +08:00
tarsin dae7dacf3d Rework SimilarityScan 2022-12-05 11:43:52 +08:00
tarsin b69b72cd1b Resolve #489 2022-12-05 09:57:42 +08:00
tarsin 68d1e39410 Refactor dependency with lazykt 2022-12-04 22:31:02 +08:00
tarsin d4482dec23 Rename .java to .kt 2022-12-04 22:31:02 +08:00
tarsin 8f36cecc7a Cleanup Code 2022-12-04 18:23:15 +08:00
tarsin a59fbcc9c8 Thanks coil 2022-12-04 12:47:14 +08:00
tarsin 25cddb2d61 Migrate to coil imageloader, Drop Conaco and BeerBelly as well
TODO: Fix SimilarityScan
2022-12-04 12:43:44 +08:00
tarsin 9a7af235b8 Fix gif render, Resolve #485 2022-12-03 09:26:08 +08:00
FooIbar c9751ab2a2 Fix simpleTags parsing (#484)
* Fix simpleTags parsing

* Adjust for low-power tags in extended mode
2022-12-03 08:49:19 +08:00
tarsin 5e13986cc0 Prefault archive mapping page tables for small files 2022-12-02 17:33:54 +08:00
tarsin 35e2cc535e Fix #483 2022-12-02 14:24:49 +08:00
tarsin 81355a0248 Resolve #479 2022-12-02 12:03:39 +08:00
tarsin 3e98502e2d Resolve #480 2022-12-02 12:00:04 +08:00
txyyh 6973fa4538 Release 1.8.3.3
Releases / Build (push) Has been cancelled
2022-12-01 12:40:59 +00:00
renovate[bot] 20191c3f46 Update dependency gradle to v8.0-milestone-4 (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 19:03:44 +08:00
tarsin f084fec54e No more compress toolbar 2022-12-01 10:20:15 +08:00
tarsin 367e3b3a93 Res cleanup 2022-12-01 09:57:59 +08:00
tarsin 36383b6f47 Use Primary color for icons 2022-12-01 09:53:18 +08:00
tarsin 9ecd14820e Use Tertiary color for big icons 2022-12-01 09:48:45 +08:00
tarsin be552e92a1 Fix #476 2022-12-01 09:25:55 +08:00
tarsin 1bf676fd56 Upgrade AGP 2022-12-01 09:12:57 +08:00
tarsin 154a96b5fd Apply blur with themes 2022-12-01 08:52:27 +08:00
FooIbar 443c384f41 Fixup! Migrate to java.time 2022-11-29 08:34:53 +08:00
tarsin 85c7829636 Robust for partially decode, Resolve #469, Resolve #470 2022-11-29 08:33:10 +08:00
tarsin 68e3bb8d66 Fix backpresscallback 2022-11-28 11:58:17 +08:00
tarsin 63b2e52eaf DomainFronting tweaks 2022-11-28 10:03:30 +08:00
tarsin c7492f3faf Rename .java to .kt 2022-11-28 10:03:30 +08:00
tarsin 9052343ef7 Upgrade build-tools 2022-11-28 10:03:30 +08:00
tarsin 00c09e4a1a Remove outdated workaround 2022-11-28 10:03:30 +08:00
FooIbar 086c0d9f28 Migrate to java.time 2022-11-28 07:00:54 +08:00
FooIbar 3ca0189f28 Drop Jetifier 2022-11-28 07:00:54 +08:00
tarsin 8f5a2ba85d Relax limit since we could scale it 2022-11-26 15:39:55 +08:00
tarsin 1ab9811d7a Don't let decoded image resolution out of screen resolution, Resolve #460 2022-11-26 15:10:03 +08:00
tarsin 0b720848ba Unpin an ip 2022-11-26 13:24:14 +08:00
tarsin eebd95e700 Fix download margin 2022-11-26 13:18:39 +08:00
tarsin 913e02f0fa translations tweaks 2022-11-26 13:15:09 +08:00
tarsin 10fdab2473 Show comments by default 2022-11-26 13:10:34 +08:00
小白-白 26710761ed Fix wrong timezone 2022-11-26 09:32:47 +08:00
tarsin 17bf6ac12b Delete 1.7 from renovate bot 2022-11-26 00:31:36 +08:00
tarsin 6a01b7729d fixup! 039e817002 2022-11-25 11:12:00 +08:00
小白-白 039e817002 Support custom preview number, Resolve #455 2022-11-25 10:24:52 +08:00
小白-白 3ea1e1914a Support hide comments, Resolve $454 2022-11-25 10:13:14 +08:00
tarsin e6c5152123 Allow obfuscate some parts 2022-11-25 10:06:16 +08:00
tarsin 7bdaf186f3 Drop Compat for Webview 2022-11-25 10:06:16 +08:00
Edward Wu b6b4e826cb Ehviewer: Translate: Update zh-rTW Translate (11/25)
* Update string from 1d8ab51 (Res cleanup)

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-11-25 09:54:24 +08:00
小白-白 949f3cb246 Fix jump on tag and uploader page 2022-11-24 12:03:26 +08:00
FooIbar 501855a8c0 Replace "..." with ellipsis character 2022-11-23 00:07:36 +08:00
FooIbar 44634be13d Remove untranslatable string 2022-11-23 00:07:36 +08:00
FooIbar 7c718df1af Use app:showAsAction with appcompat 2022-11-23 00:07:36 +08:00
FooIbar 16cca9cbbd Remove redundant namespace 2022-11-23 00:07:36 +08:00
FooIbar b715320298 android:forceDarkAllowed requires API 29 2022-11-23 00:07:36 +08:00
FooIbar d31d9eb137 WRITE_EXTERNAL_STORAGE is not required on API 29 2022-11-23 00:07:36 +08:00
tarsin b53c29c035 Fix fab position 2022-11-22 21:26:13 +08:00
tarsin d3bf20bee6 Refresh tokens, fuck github 2022-11-22 19:57:00 +08:00
tarsin cff445ddf5 Use latest mdc for searchbar migration 2022-11-22 19:47:30 +08:00
tarsin 877d96a50a Release 1.8.3.2
Releases / Build (push) Has been cancelled
2022-11-22 16:19:20 +08:00
tarsin 12863dfa21 Fixup! Fix comments insets 2022-11-22 15:10:34 +08:00
tarsin 959959d880 Fixup! Fix comments insets 2022-11-22 15:06:51 +08:00
tarsin cd676eabfc Mitigate CCP's sanctions 2022-11-22 14:57:58 +08:00
tarsin 0a3e95c8d4 Rename .java to .kt 2022-11-22 14:57:58 +08:00
tarsin 35b483e655 Harmonize shortcut color 2022-11-22 13:18:56 +08:00
tarsin 7dae7b72f4 Checkout libarchive to latest 2022-11-22 12:43:28 +08:00
tarsin 620f5df241 Checkout liblzma to v5.2.8 2022-11-22 12:38:07 +08:00
tarsin d5ddb6eb8c Fix comments insets 2022-11-22 11:52:23 +08:00
txyyh 33c9b9d4e4 Release 1.8.3.1
Releases / Build (push) Has been cancelled
2022-11-21 12:39:03 +00:00
txyyh 216cafbe54 Revert "Update README.md"
This reverts commit a37c676d96.
2022-11-21 12:34:17 +00:00
txyyh 72a644bd3b Remove Android version note 2022-11-21 12:10:44 +00:00
tarsin 7008f0bc89 Better solution for #440 2022-11-21 20:09:24 +08:00
tarsin 289d3ea287 Resolve #440 2022-11-21 19:47:21 +08:00
tarsin 92ae157f00 Update en.md 2022-11-21 16:56:11 +08:00
tarsin 9800f7c01b Update zh-tw.md 2022-11-21 16:55:44 +08:00
tarsin 8281af4590 Update zh-cn.md 2022-11-21 16:55:24 +08:00
tarsin d609e5e113 Update README.md 2022-11-21 16:55:01 +08:00
tarsin 238d769f27 Release 1.8.3.0
Releases / Build (push) Has been cancelled
Just stop spamming for something like "I can install", If U still can't install then please SMASH YOUR DEVICE OR REPLACE YOUR GODDAMN SYSTEM
2022-11-21 16:48:50 +08:00
tarsin 1e648cc6b4 Nullability checks
extractToByteBuffer may return null object for some reason
2022-11-21 12:15:23 +08:00
tarsin 7ac20d59fc Use DirectByteBuffer
Seems my mimalloc(I use it as Android's system allocator) doesn't like release memory to system since I replace MADV_DONTNEED with MADV_FREE
https://github.com/AcmeUI/android_external_mimalloc
but there is no memleak I sure
2022-11-21 12:11:35 +08:00
tarsin 1d8ab51a22 Res cleanup 2022-11-20 21:06:11 +08:00
tarsin cdfb3b74b3 Code cleanup 2022-11-20 20:57:16 +08:00
小白-白 6f2868f7c3 Support select all on fav page (#437) 2022-11-20 19:18:43 +08:00
tarsin f3061fa497 Kotlin 1.8 and Java 19 2022-11-20 19:09:27 +08:00
FooIbar 468cce5f3d Remove fix thumb url (#431) 2022-11-20 10:03:21 +08:00
小白-白 bfaba80d3b Fix backup online fav list to local 2022-11-20 09:09:36 +08:00
小白-白 2ca4a2af86 Restrict datepicker ranges with validators 2022-11-20 09:09:36 +08:00
tarsin 7d0b245d3d Unroll loops 2022-11-20 00:09:00 +08:00
tarsin 96fcf69bdd Guard blur method 2022-11-19 23:01:44 +08:00
tarsin 63d95b1265 Guard some methods 2022-11-19 22:57:34 +08:00
tarsin d173e83e01 Resolve #421 2022-11-19 22:24:38 +08:00
tarsin 54d11dea95 Initial migrate to ImageDecoder Java api, and downgrade minsdk to 28 2022-11-19 22:17:08 +08:00
tarsin fbdab389c4 Let's target JVM 18 2022-11-17 23:39:06 +08:00
tarsin 3dd3bad362 Resolve #393 2022-11-17 21:54:05 +08:00
tarsin 65a9a4b4f2 Make Settings appbar liftonscroll 2022-11-17 21:34:28 +08:00
tarsin 5f6cf5550d Rename .java to .kt 2022-11-17 21:34:28 +08:00
tarsin 673dad4032 Fix comment send ui color 2022-11-17 19:33:12 +08:00
tarsin 5e45ec9a9c Restrict datepicker ranges 2022-11-17 18:11:02 +08:00
tarsin 306eb9d23c Bump version
Releases / Build (push) Has been cancelled
2022-11-17 12:36:19 +08:00
tarsin 158218e501 No jump menu when cannot jump pages 2022-11-17 12:22:19 +08:00
tarsin d773332b74 Use stable version of mdc 2022-11-17 12:21:59 +08:00
tarsin 5dc2eb0062 Expand title on CollapsingToolbarLayout 2022-11-17 11:31:40 +08:00
tarsin bee7febafc Use compressed toolbar 2022-11-17 00:45:38 +08:00
tarsin f49cc2d498 Fix GalleryCommentsScene fab and edittext render out of screen 2022-11-17 00:34:10 +08:00
tarsin 3f4d2b75fc Correctly use attach behaviour, not padding 2022-11-16 23:48:20 +08:00
tarsin af8733c2d1 Explicitly set LiftOnScrollTargetView 2022-11-16 22:46:24 +08:00
tarsin 545eed98a7 Support lift Appbar when scroll, use CollapsingAppbar on gallery detail for long title 2022-11-16 21:50:31 +08:00
tarsin 2da436189a Allow overscroll on GalleryDetailScene, also set title on appbar, Resolve #277 2022-11-16 17:26:27 +08:00
tarsin 906ea11733 Rename .java to .kt 2022-11-16 17:26:27 +08:00
小白-白 47c5629f7d Fix fav page load after jump 2022-11-16 11:52:41 +08:00
tarsin a37c676d96 Update README.md 2022-11-15 22:36:16 +08:00
tarsin d9981f15f2 Bump version
Releases / Build (push) Has been cancelled
2022-11-15 22:19:21 +08:00
tarsin 27b75746fd Simplify clipboardutils 2022-11-15 22:17:12 +08:00
tarsin ee80a3a841 Copy to Clipboard refactor 2022-11-15 22:05:26 +08:00
tarsin f8d639054f Just use normal rv 2022-11-15 21:48:22 +08:00
tarsin 7f76e58e4e Move GalleryInfoBottomSheet out of scene package to allow obfuscate and repackage 2022-11-15 21:46:00 +08:00
tarsin 1b326a86a2 Convert GalleryInfoScene to BottomSheet, Resolve #354 2022-11-15 21:38:27 +08:00
tarsin 607bf4c67f Use inverse color on snackbar, #326 2022-11-15 15:30:44 +08:00
小白-白 3e4836c91e Support favorites prev page load 2022-11-15 12:54:41 +08:00
小白-白 dae8f21388 Support prev page load 2022-11-15 12:54:41 +08:00
小白-白 84b328b60b Init prev page load 2022-11-15 12:54:41 +08:00
小白-白 b98d55b444 Hide jump button on local fav cat 2022-11-15 12:54:41 +08:00
小白-白 ee9413e54a Fix favorite page jump 2022-11-15 12:54:41 +08:00
小白-白 76dedce5da Remove unavailable search options 2022-11-15 12:54:41 +08:00
tarsin 37f99ae706 Fuck off the "Press twice to exit"
It can't be more stupid on our modern gesture navigation
2022-11-14 19:34:37 +08:00
tarsin aee58d967d Kotlin 1.7.21 2022-11-14 14:42:41 +08:00
tarsin 5ecabeb045 Rearrange gallery actions, resolve #413 2022-11-14 14:29:02 +08:00
tarsin 408c142aad Haptic feedback for slider, #408 2022-11-14 14:09:54 +08:00
tarsin a56d541380 fixup! Adjust madvise usage 2022-11-10 15:28:06 +08:00
tarsin ae79417845 Adjust madvise usage 2022-11-10 15:21:17 +08:00
小白-白 010daa583e Remove unavailable advanced search options 2022-11-10 12:36:08 +08:00
renovate[bot] 3034957c99 Update room_version to v2.5.0-beta02 (1.8.x.x) (#402)
Update room_version to v2.5.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-10 08:23:57 +08:00
renovate[bot] 99134346ed Update dependency androidx.compose.material3:material3 to v1.0.1 (1.8.x.x) (#399)
Update dependency androidx.compose.material3:material3 to v1.0.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-10 08:23:00 +08:00
renovate[bot] 9a70708300 Update compose_version to v1.3.1 2022-11-09 23:13:03 +00:00
tarsin 50f0df5012 Explicitly set format and filter we use
This helps to reduce native library size
2022-11-10 00:56:49 +08:00
小白-白 599bb80106 Fix toplist page load 2022-11-09 23:15:38 +08:00
小白-白 aa0db1f400 Hide jump button on the popular page 2022-11-09 23:15:38 +08:00
skymkmk 82b6fd22a9 Enhancement: Add delete image file option to the dialog on the gallery page (#391) 2022-11-08 15:26:29 +08:00
renovate[bot] b5c730d37a Update plugin com.android.application to v8.0.0-alpha08 (1.8.x.x) (#388)
Update plugin com.android.application to v8.0.0-alpha08

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 04:36:48 +00:00
txyyh a2cb6444f4 Revert "Remove preview count limit #332 (#333)"
This reverts commit 783fa10157.
2022-11-07 10:28:34 +00:00
tarsin 8a144eabca Reset ContentLayout changes
This component is quite a shit, further we must rewrite it to better adapt new page-off gid-based load design
2022-11-07 18:26:27 +08:00
tarsin 75c2a3b0ec Tweak HistoryScene with modern kotlin 2022-11-06 23:19:15 +08:00
tarsin beec02a69b Fix #370 2022-11-06 21:59:08 +08:00
tarsin df78b45f69 Initial support for jump to certain date 2022-11-06 14:58:05 +08:00
tarsin e6a3c8f165 Rename .java to .kt 2022-11-06 14:58:05 +08:00
tarsin 4d948e93a3 Datepicker stub 2022-11-06 13:23:49 +08:00
tarsin ece082727b Release 1.8.3.0-alpha04
Releases / Build (push) Has been cancelled
2022-11-06 13:02:55 +08:00
tarsin 5b5040f4af No gallery found no more a exception 2022-11-06 13:02:05 +08:00
tarsin d9d88e690b Fix favourite list load first 2022-11-06 12:57:51 +08:00
tarsin bda123fc39 Build with JDK 19
Gradle 8.0.0-M3 should be able to work with JDK19
2022-11-06 00:22:59 +08:00
tarsin 2200bb9b45 Upgrade java compatibility to Java 17 2022-11-06 00:21:25 +08:00
txyyh 884d460b52 Rel 1.8.3.0-alpha03
Releases / Build (push) Has been cancelled
2022-11-05 09:50:13 +00:00
tarsin beb97270a0 Initial support for predictive back gesture
https://developer.android.com/guide/navigation/predictive-back-gesture
2022-11-05 12:48:03 +08:00
小白-白 52681bd386 Fix favorite page load (#368)
修复收藏页面按发布时间排序时无法翻页问题
2022-11-04 11:40:30 +08:00
renovate[bot] d772a737fc Update plugin com.google.devtools.ksp to v1.7.20-1.0.8 (1.8.x.x) (#367)
Update plugin com.google.devtools.ksp to v1.7.20-1.0.8

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-04 11:15:20 +08:00
tarsin 638fcfea12 Do not cancel a sent call
Should fix slow load speed
2022-11-04 00:22:25 +08:00
tarsin cae32cca5a Rel 1.8.3.0-alpha02
Releases / Build (push) Has been cancelled
2022-11-03 23:19:06 +08:00
tarsin c429e7054a Don't parse found in common path 2022-11-03 23:11:33 +08:00
tarsin f526f11c73 Fix tag search page load 2022-11-03 22:59:27 +08:00
tarsin 892016368f Fix favourite scene data restore 2022-11-03 22:54:28 +08:00
tarsin d9464ac278 Fix uploader page request 2022-11-03 16:20:47 +08:00
tarsin a68a9995a1 Rel 1.8.3.0-alpha01
Releases / Build (push) Has been cancelled
2022-11-03 16:07:16 +08:00
tarsin 1a0c2cb180 Relax founds limit
We will calculate accurately later
2022-11-03 16:01:50 +08:00
tarsin d7572f5a79 Fix restore state 2022-11-03 15:48:35 +08:00
tarsin a208651e13 Code clean and allow refresh pages 2022-11-03 15:31:31 +08:00
tarsin 41c231b4f3 Bump version to 1.8.3.0-alpha
Releases / Build (push) Has been cancelled
2022-11-03 14:13:32 +08:00
tarsin 567b6f7659 Minimal changes to rework Favourite grabs
TODO: Reimpl goto, refresh pages
And take care of per page counts by user
2022-11-03 13:18:38 +08:00
tarsin 6d1e0089e5 Minimal changes to rework new exhentai page load
TODO: Reimpl Favourite grab, goto, refresh pages
2022-11-03 11:48:04 +08:00
renovate[bot] 502f532fe7 Update plugin com.android.application to v8.0.0-alpha07 (1.8.x.x) (#357)
Update plugin com.android.application to v8.0.0-alpha07

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 19:24:59 +00:00
tarsin ddc47f09ab Resolve #348 2022-11-01 11:18:44 +08:00
tarsin 5dbcdf2fce Use thinlto 2022-11-01 00:00:07 +08:00
tarsin 706588c39c Kotlinize 2022-10-31 23:53:44 +08:00
tarsin 3e3e64942e Rename .java to .kt 2022-10-31 23:53:44 +08:00
renovate[bot] 3700ac8a84 Update dependency gradle to v8.0-milestone-3 (1.8.x.x) (#350)
Update dependency gradle to v8.0-milestone-3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-28 23:05:06 +08:00
renovate[bot] cc0dc0abf5 Update lifecycle_version to v2.6.0-alpha03 (1.8.x.x) (#340)
Update lifecycle_version to v2.6.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:55 +08:00
renovate[bot] 5861147d17 Update dependency androidx.paging:paging-runtime to v3.2.0-alpha03 (1.8.x.x) (#338)
Update dependency androidx.paging:paging-runtime to v3.2.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:42 +08:00
renovate[bot] 732035a0e2 Update dependency com.google.android.material:material to v1.8.0-alpha02 (1.8.x.x) (#339)
Update dependency com.google.android.material:material to v1.8.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:28 +08:00
renovate[bot] 643f71009d Update nav_version to v2.5.3 (1.8.x.x) (#341)
Update nav_version to v2.5.3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:14:12 +08:00
renovate[bot] 4643e37adb Update plugin com.android.application to v8.0.0-alpha06 (1.8.x.x) (#342)
Update plugin com.android.application to v8.0.0-alpha06

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 12:13:55 +08:00
renovate[bot] 1bc8854685 Update dependency androidx.compose.material3:material3 to v1.0.0 (1.8.x.x) (#337)
Update dependency androidx.compose.material3:material3 to v1.0.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-25 02:07:28 +00:00
renovate[bot] 66ebf6a391 Update compose_version to v1.3.0 (1.8.x.x) (#336)
Update compose_version to v1.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 23:01:21 +00:00
renovate[bot] 1239277cec Update activity_version to v1.6.1 (1.8.x.x) (#335)
Update activity_version to v1.6.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-24 19:34:27 +00:00
tarsin 9cb8fb5527 Enable new resources shrinker
And full mode r8 is enabled by default on AGP8 above
2022-10-23 19:04:56 +08:00
tarsin 819d67dca8 Upgrade to gradle 8.0.0M2 2022-10-23 13:05:29 +08:00
tarsin f895982fa0 Revert to jdk 18 2022-10-23 12:45:59 +08:00
tarsin 505c097659 Build with jdk19 and enable parallel-gc
https://blog.bitrise.io/post/switch-to-jdk-17-parallel-gc-and-speed-up-your-android-builds-by-9-20
2022-10-23 12:35:05 +08:00
小白-白 783fa10157 Remove preview count limit #332 (#333)
fix: preview count limit
2022-10-23 01:25:06 +08:00
tarsin 82ffbf2a1d Reset unlock status if within delay time 2022-10-20 13:33:50 +08:00
renovate[bot] 3c3ae47511 Update plugin com.android.application to v8.0.0-alpha05 (1.8.x.x) (#331)
Update plugin com.android.application to v8.0.0-alpha05

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-18 19:44:59 +00:00
tarsin 858d7acf68 Fix code style 2022-10-17 00:43:35 +08:00
renovate[bot] f8b7005690 Update plugin com.android.application to v8.0.0-alpha04 (1.8.x.x) (#330)
Update plugin com.android.application to v8.0.0-alpha04

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-15 03:29:10 +00:00
renovate[bot] 33a8cb98e6 Update plugin com.google.devtools.ksp to v1.7.20-1.0.7 (1.8.x.x) (#329)
Update plugin com.google.devtools.ksp to v1.7.20-1.0.7

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-14 02:17:50 +00:00
tarsin 338a22f2ed No need to trigger gc manually 2022-10-09 14:44:42 +08:00
tarsin f7cecaf709 on stack tile buffer 2022-10-08 21:05:42 +08:00
tarsin c38b3c15e7 Tweak tags color 2022-10-08 20:48:40 +08:00
tarsin 4665678e9f fixup! Use MaterialIconButton 2022-10-08 20:35:43 +08:00
tarsin 07416eb74d Use MaterialIconButton 2022-10-07 19:20:01 +08:00
renovate[bot] 6f37dc53c7 Update plugin com.android.application to v8.0.0-alpha03 (1.8.x.x) (#327)
Update plugin com.android.application to v8.0.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-07 02:07:07 +00:00
tarsin 55b963dcf9 Kotlin 1.7.20 2022-10-06 12:29:35 +08:00
txyyh 1a433019b3 Add strikethrough (#325) 2022-10-06 12:21:02 +08:00
renovate[bot] 7b292cb1ab Update dependency androidx.compose.material3:material3 to v1.0.0-rc01 (1.8.x.x) (#322)
Update dependency androidx.compose.material3:material3 to v1.0.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 11:07:03 +08:00
renovate[bot] b2f5590c04 Update fragment_version to v1.6.0-alpha03 (1.8.x.x) (#323)
Update fragment_version to v1.6.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 09:36:07 +08:00
renovate[bot] 2cf2c87a8b Update room_version to v2.5.0-beta01 (1.8.x.x) (#324)
Update room_version to v2.5.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-06 09:35:54 +08:00
renovate[bot] 01420f7e72 Update compose_version to v1.3.0-rc01 (1.8.x.x) (#321)
Update compose_version to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-05 23:01:01 +00:00
txyyh 3f699973b9 Release 1.8.2.4
Releases / Build (push) Has been cancelled
2022-10-05 14:25:33 +00:00
renovate[bot] 3f27c9139c Update plugin com.android.application to v8.0.0-alpha02 (1.8.x.x) (#313)
Update plugin com.android.application to v8.0.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-27 22:42:23 +00:00
tarsin 9eee6fc4af Clipboard management refactor, kill all tips when copy to clipboard on Tiramisu above 2022-09-27 00:45:23 +08:00
tarsin a343dbe8f8 Rename .java to .kt 2022-09-27 00:45:23 +08:00
tarsin bc08a09621 Fix read archive 2022-09-23 09:20:15 +08:00
tarsin 586ad66bff Upgrade to AGP 8
And avoid annoying cmake warnings as well
2022-09-22 20:29:53 +08:00
tarsin 1a426e1f5f Simplify code 2022-09-22 20:29:18 +08:00
tarsin e9a9f1070c Kotlinize 2022-09-22 20:29:18 +08:00
tarsin 7d3dd61bc4 Rename .java to .kt 2022-09-22 20:29:18 +08:00
老虎豆 f37894af88 Fix when clicking cover in download that don't enter gallery detail(#251) (#305)
Fix when clicking cover in download that don't enter gallery detail
2022-09-22 19:41:56 +08:00
renovate[bot] 14129545b8 Update dependency androidx.recyclerview:recyclerview to v1.3.0-rc01 (1.8.x.x) (#297)
Update dependency androidx.recyclerview:recyclerview to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 10:30:25 +08:00
renovate[bot] f06346b6c7 Update dependency androidx.customview:customview to v1.2.0-alpha02 (1.8.x.x) (#296)
Update dependency androidx.customview:customview to v1.2.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 09:51:53 +08:00
renovate[bot] 13e3e8c534 Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha04 (1.8.x.x) (#295)
Update dependency androidx.constraintlayout:constraintlayout to v2.2.0-alpha04

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 08:14:13 +08:00
renovate[bot] 4be282813c Update dependency androidx.compose.material3:material3 to v1.0.0-beta03 (1.8.x.x) (#294)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 08:14:01 +08:00
renovate[bot] 74e66a407d Update compose_version to v1.3.0-beta03 (1.8.x.x) (#291)
Update compose_version to v1.3.0-beta03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:55:08 +08:00
renovate[bot] b19da4c82e Update dependency androidx.annotation:annotation to v1.5.0 (1.8.x.x) (#292)
Update dependency androidx.annotation:annotation to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:54:41 +08:00
renovate[bot] 2f49764e1b Update dependency androidx.biometric:biometric-ktx to v1.2.0-alpha05 (1.8.x.x) (#293)
Update dependency androidx.biometric:biometric-ktx to v1.2.0-alpha05

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-22 06:54:23 +08:00
renovate[bot] 08dfb0f72d Update activity_version to v1.6.0 (1.8.x.x) (#290)
Update activity_version to v1.6.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-21 21:38:55 +00:00
txyyh 9251ed238c Fix archive code error (#289) 2022-09-21 08:56:56 +08:00
tarsin 3b96328765 Use fixed size buffer 2022-09-20 18:29:35 +08:00
tarsin 108928e524 Implement save, share image from archive 2022-09-20 18:09:41 +08:00
LaLucid b1191bbf9b Add Gradle cache clean (#287) 2022-09-18 18:00:24 +08:00
txyyh 3424260cfd Release 1.8.2.3
Releases / Build (push) Has been cancelled
2022-09-18 03:31:58 +00:00
EdwardWu e1fe56160e Ehviewer: Translate: Update zh-rTW Translate (09/17) (#284)
* Update string from beadc7 (Merge Account and Signout)
* Update string from 1a6e03 (Add Lock Delay)

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-18 01:26:10 +08:00
tarsin 5838ea3e3f Fix #282 2022-09-17 15:11:34 +08:00
txyyh 0906b17ed1 Use example version code in issue template 2022-09-17 13:44:56 +08:00
txyyh 49b9cb72d3 Apply format to all docs 2022-09-16 12:40:28 +00:00
tarsin 01508cc3c1 Update README.md 2022-09-16 13:17:45 +08:00
tarsin 79eba83812 fixup! Release 1.8.2.2
Releases / Build (push) Has been cancelled
2022-09-16 12:42:27 +08:00
tarsin a455dee24c Release 1.8.2.2 2022-09-16 12:41:39 +08:00
tarsin f58b4959e4 Reformat code 2022-09-16 12:39:50 +08:00
tarsin a6b4182ecd Don't require External storage [1.8.x.x] 2022-09-16 12:37:06 +08:00
tarsin 9010591ffc Import Download label first, Fix #245 2022-09-16 11:53:48 +08:00
tarsin e0dde2c982 Read database permissively, Fix #274 2022-09-16 11:40:02 +08:00
tarsin 6109d97bf7 Do not record last leave time if app not unlocked, And Fix #275 2022-09-16 11:14:11 +08:00
tarsin 95936e01ce Drop nativelib version dialog 2022-09-16 10:50:04 +08:00
tarsin beadc7b185 Merge Account and Signout 2022-09-16 10:46:47 +08:00
tarsin 0bc2e45137 Merge Account and Cookies 2022-09-16 10:35:40 +08:00
tarsin b8e649e995 Add support to visit login status 2022-09-16 10:30:16 +08:00
tarsin bdcd21a535 Rename .java to .kt 2022-09-16 10:30:15 +08:00
tarsin 1b779a8b77 Disable unlock delay seekbar as well if Authenticators not available 2022-09-16 09:52:16 +08:00
renovate[bot] 6c59a341ee Update plugin com.android.application to v7.3.0 (1.8.x.x) (#279)
Update plugin com.android.application to v7.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-15 22:37:00 +00:00
老虎豆 1a6e03c8f8 Add Lock Delay(#276) (#278)
* Add Lock Delay(#276)

* Remove zh-TW translation

* Use SeekBarPreference instead of SimpleMenuPreference
2022-09-16 02:24:49 +08:00
txyyh 653c0a2ac6 Add thanks to issue template (#272)
* Add thanks

* Shorten description
2022-09-12 18:51:43 +08:00
renovate[bot] e4189f367a Update nav_version to v2.5.2 (1.8.x.x) (#265)
Update nav_version to v2.5.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 20:11:10 +00:00
renovate[bot] 48b7355ba4 Update dependency androidx.compose.material3:material3 to v1.0.0-beta02 (1.8.x.x) (#263)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 15:35:49 +08:00
renovate[bot] d7d3a903ff Update lifecycle_version to v2.6.0-alpha02 (1.8.x.x) (#264)
Update lifecycle_version to v2.6.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 15:35:36 +08:00
renovate[bot] bab3067273 Update core_version to v1.9.0 (1.8.x.x) (#259)
Update core_version to v1.9.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:32:34 +08:00
renovate[bot] fdd70a6908 Update dependency androidx.annotation:annotation to v1.5.0-rc01 (1.8.x.x) (#260)
Update dependency androidx.annotation:annotation to v1.5.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:32:22 +08:00
renovate[bot] 3699a8fe30 Update dependency androidx.annotation:annotation-experimental to v1.3.0 (1.8.x.x) (#261)
Update dependency androidx.annotation:annotation-experimental to v1.3.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:30:05 +08:00
renovate[bot] 32f98f8a15 Update dependency androidx.appcompat:appcompat to v1.6.0-rc01 (1.8.x.x) (#262)
Update dependency androidx.appcompat:appcompat to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 10:27:18 +08:00
renovate[bot] c5366760a2 Update compose_version to v1.3.0-beta02 (1.8.x.x) (#258)
Update compose_version to v1.3.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-08 02:03:25 +00:00
renovate[bot] c98ec2a7a4 Update activity_version to v1.6.0-rc02 (1.8.x.x) (#257)
Update activity_version to v1.6.0-rc02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-07 21:47:13 +00:00
txyyh e4c729e51b Add additional context textarea (#255)
* Use other word

* Add additional context to bug report
2022-09-07 08:31:10 +00:00
EdwardWu f36726d8fb Ehviewer: Translate: Update zh-rTW Translate (09/06) (#254)
* Update string from d2fc2e
* Update some strings translate

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-06 13:44:49 +08:00
LittleLoli d2fc2ee136 add backup remote favorite list to local. (#252) 2022-09-06 01:21:53 +08:00
EdwardWu 6edc2164af Ehviewer: Translate: Update zh-rTW Translate (09/04) (#250)
* Update string from a5161bb

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-09-04 23:55:22 +08:00
txyyh e34dc30f1f Update material-components (#249) 2022-09-03 08:21:16 +08:00
tarsin c03081d3fa Fix #246 2022-08-31 08:48:36 +08:00
renovate[bot] 97287e4538 Update dependency com.google.android.material:material to v1.7.0-rc01 (1.8.x.x) (#247)
Update dependency com.google.android.material:material to v1.7.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-31 08:43:42 +08:00
txyyh fc44507a34 Use label on info card (#243) 2022-08-29 16:24:26 +08:00
txyyh 2597004750 Use LabelLarge on CardMessage (#244) 2022-08-29 16:24:11 +08:00
txyyh d1bd9c5fae Use title small on comment (#242) 2022-08-29 16:23:32 +08:00
renovate[bot] 54542f9246 Update room_version to v2.5.0-alpha03 (1.8.x.x) (#236)
Update room_version to v2.5.0-alpha03

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 14:40:38 +08:00
renovate[bot] 377b23458f Update fragment_version to v1.6.0-alpha02 (1.8.x.x) (#235)
Update fragment_version to v1.6.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 14:40:13 +08:00
renovate[bot] b7973cdb76 Update dependency androidx.webkit:webkit to v1.5.0 (1.8.x.x) (#234)
Update dependency androidx.webkit:webkit to v1.5.0

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 06:24:40 +00:00
renovate[bot] 5f1a70886c Update dependency androidx.annotation:annotation-experimental to v1.3.0-rc01 (1.8.x.x) (#232)
Update dependency androidx.annotation:annotation-experimental to v1.3.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:49:36 +08:00
renovate[bot] 5fbc8b0b66 Update dependency androidx.compose.material3:material3 to v1.0.0-beta01 (1.8.x.x) (#233)
Update dependency androidx.compose.material3:material3 to v1.0.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:49:24 +08:00
renovate[bot] 780b2b6e1d Update compose_version to v1.3.0-beta01 (1.8.x.x) (#229)
Update compose_version to v1.3.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:45:25 +08:00
renovate[bot] 039ad2032c Update core_version to v1.9.0-rc01 (1.8.x.x) (#230)
Update core_version to v1.9.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:45:05 +08:00
renovate[bot] a24222b693 Update dependency androidx.annotation:annotation to v1.5.0-beta01 (1.8.x.x) (#231)
Update dependency androidx.annotation:annotation to v1.5.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-25 07:44:53 +08:00
renovate[bot] 6047d91d7e Update activity_version to v1.6.0-rc01 (1.8.x.x) (#228)
Update activity_version to v1.6.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 22:18:38 +00:00
tarsin f0158f6946 Make dialog takes some blur 2022-08-24 21:00:41 +08:00
tarsin 00ecdc58e6 Upgrade ndk to 25.1.8937393 2022-08-24 18:04:55 +08:00
renovate[bot] 0a075a97af Update dependency org.jsoup:jsoup to v1.15.3 (1.8.x.x) (#225)
Update dependency org.jsoup:jsoup to v1.15.3

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 05:56:11 +00:00
tarsin 34ee70ca7d Make download card takes proper round corner 2022-08-23 19:38:53 +08:00
tarsin 338751ef2b Drop unused code 2022-08-23 19:34:04 +08:00
tarsin d4465ba864 Drop unused color res
* Color should be dynamic most time
2022-08-23 19:11:23 +08:00
tarsin 9a8fdd4edb Make swiperefreshlayout and ProgressIndicator follow dyncolor 2022-08-23 19:05:10 +08:00
txyyh 338848f1eb Make release
Releases / Build (push) Has been cancelled
2022-08-22 11:01:25 +00:00
tarsin 9b918ec036 Revert "Reformat Code"
This reverts commit e0f7dab34f.
2022-08-22 17:27:52 +08:00
txyyh 24439c7853 Update screenshots 2022-08-22 07:20:07 +00:00
txyyh a7f63a5b57 Make release
Releases / Build (push) Has been cancelled
2022-08-22 05:02:51 +00:00
tarsin 1e68782056 Use Tiramisu's imageselector 2022-08-22 12:24:12 +08:00
tarsin e0f7dab34f Reformat Code 2022-08-22 11:38:03 +08:00
tarsin 5513d47b6d Fix #215 2022-08-22 11:30:25 +08:00
tarsin e12abb66f7 Rename .java to .kt 2022-08-22 11:30:25 +08:00
tarsin 106be8c0ca Nuke in-app locale selector on Tiramisu above 2022-08-22 10:18:17 +08:00
txyyh 2e0d065e4d Implement #128 (#217)
* Implement #128

* Add LICENSE Header

* Fix zh_CN zh_TW
2022-08-22 10:06:10 +08:00
tarsin 073298cfee Implement #127 2022-08-22 08:49:57 +08:00
tarsin ebd0f4b4f8 Rename .java to .kt 2022-08-22 08:49:57 +08:00
tarsin 0ba71b060c Fix a race condition 2022-08-21 12:31:10 +08:00
tarsin 144e384441 Refine error handling 2022-08-21 12:00:29 +08:00
tarsin 9bb694171b Implement multithreading archive read 2022-08-21 09:21:53 +08:00
tarsin 647362cd82 Implement multiple archive context, Fix #213
* This is useful for multithreading decompress with lock usage
2022-08-20 23:38:13 +08:00
tarsin 56996558df Fix a use-after-free 2022-08-20 20:23:57 +08:00
txyyh 956b7c719e Make release
Releases / Build (push) Has been cancelled
2022-08-15 14:30:55 +00:00
txyyh 56e13559a0 Describe CI pages 2022-08-15 14:27:45 +00:00
txyyh 261ed86047 Rename apks on releases 2022-08-15 14:24:48 +00:00
tarsin d9b18358d2 Fix a wrong place res 2022-08-15 22:00:19 +08:00
tarsin a5161bba6f Support resize thumb, Implement #193 2022-08-15 21:52:49 +08:00
tarsin 62b00c758c IsLightStatusBar always follow night mode, Fix #199 2022-08-15 20:42:44 +08:00
tarsin 8663b4fa40 Rename .java to .kt 2022-08-15 20:42:44 +08:00
tarsin 6304c12bd0 Fix #196 2022-08-15 20:05:29 +08:00
EdwardWu efe2473ecf Ehviewer: Re-Add warning (#195) 2022-08-12 22:38:46 +08:00
tarsin 6ba494a4f1 Fixup! Re-add warning
Releases / Build (push) Has been cancelled
2022-08-12 20:43:24 +08:00
tarsin b81572ffff Re-add warning 2022-08-12 20:40:13 +08:00
tarsin d2ae1567be import jetpack compose 2022-08-12 19:01:59 +08:00
tarsin d00055a5f8 Add back doh 2022-08-12 18:09:59 +08:00
tarsin 896a2684d9 Make release 2022-08-12 17:28:13 +08:00
tarsin b98ecb17d3 Checkout to nettle 3.8.1 2022-08-12 17:24:31 +08:00
tarsin cd9af7feee Silence a warning 2022-08-12 15:29:39 +08:00
tarsin 1c38f77b5c Refactor history with room paging 2022-08-12 15:29:04 +08:00
tarsin c5977e358e Rename .java to .kt 2022-08-12 15:29:04 +08:00
tarsin 78e58e6894 Import android paging 2022-08-12 12:02:54 +08:00
tarsin 30433c5cbf Drop history limit 2022-08-12 12:00:50 +08:00
renovate[bot] fe8e3766c0 Update dependency com.google.android.material:material to v1.7.0-beta01 2022-08-11 22:02:01 +00:00
txyyh 90c95d9788 Enable renovate automerge 2022-08-11 09:57:41 +00:00
tarsin 3930a79a47 Update AndroidManifest.xml 2022-08-11 14:17:32 +08:00
renovate[bot] fc852780f8 Update activity_version to v1.6.0-beta01 (1.8.x.x) (#177)
Update activity_version to v1.6.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:58 +08:00
renovate[bot] 35315c38cd Update dependency androidx.annotation:annotation to v1.5.0-alpha02 (1.8.x.x) (#179)
Update dependency androidx.annotation:annotation to v1.5.0-alpha02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:41 +08:00
renovate[bot] 0b40f37418 Update dependency androidx.webkit:webkit to v1.5.0-rc01 (1.8.x.x) (#183)
Update dependency androidx.webkit:webkit to v1.5.0-rc01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:11:07 +08:00
renovate[bot] 5a5f0421d8 Update dependency androidx.annotation:annotation-experimental to v1.3.0-beta01 (1.8.x.x) (#180)
Update dependency androidx.annotation:annotation-experimental to v1.3.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:10:38 +08:00
renovate[bot] 86c5c5bc59 Update dependency androidx.appcompat:appcompat to v1.6.0-beta01 (1.8.x.x) (#181)
Update dependency androidx.appcompat:appcompat to v1.6.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:08:04 +08:00
renovate[bot] 776a088de9 Update core_version to v1.9.0-beta01 (1.8.x.x) (#178)
Update core_version to v1.9.0-beta01

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:07:45 +08:00
renovate[bot] ba54655877 Update dependency androidx.recyclerview:recyclerview to v1.3.0-beta02 (1.8.x.x) (#182)
Update dependency androidx.recyclerview:recyclerview to v1.3.0-beta02

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-11 09:07:30 +08:00
txyyh 9f47ae438c Disable blank issue 2022-08-10 16:49:39 +00:00
txyyh 1717233f5a Add LICENSE header 2022-08-10 09:00:26 +00:00
txyyh cf71feb89d Use ic_launcher_foreground on scene_login (#174) 2022-08-10 16:23:09 +08:00
txyyh 21e393246a Use new icon (#173)
* Use new icon

* Add monochrome icon
2022-08-10 15:45:15 +08:00
txyyh c368f234f0 Fix wrong deploy username 2022-08-09 05:04:44 +00:00
tarsin c3d8610e2a Update AndroidManifest.xml 2022-08-09 08:43:24 +08:00
txyyh 1a10e330aa Only build CI on branches (#170) 2022-08-08 00:59:21 +08:00
txyyh d992897506 Auto deploy release to fdroid repo (#169) 2022-08-07 19:21:55 +08:00
tarsin a0fe6f0d7d Revert "archive: Use memfd instead"
* It sucks, seems simplify the conditions, but more syscalls are
  made(read/seek/write), more context switch, futhermore AImageDecoder itself does support
  read raw buffer, and we are not intend to support multi-process decoding

This reverts commit f4826ea727.
2022-08-07 19:14:25 +08:00
tarsin f4826ea727 archive: Use memfd instead 2022-08-07 17:13:48 +08:00
tarsin a17b254494 Call mmap/munmap in native code 2022-08-07 15:23:32 +08:00
tarsin 0190fc07bd Fix thumb card margin 2022-08-07 14:38:27 +08:00
tarsin c8dab6a88f Archive: ignore crc32 2022-08-07 11:43:27 +08:00
renovate[bot] fbafadfd98 Update dependency gradle to v7.5.1 (1.8.x.x) (#167)
Update dependency gradle to v7.5.1

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-06 07:42:25 +08:00
tarsin b5832fd706 Make release
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-08-05 22:13:38 +08:00
tarsin 93312a0714 Dependencies tweaks 2022-08-05 22:13:03 +08:00
tarsin 7e6e1f6f14 Make default label removable 2022-08-05 21:14:38 +08:00
tarsin 6cd498af6a Make downloadList qs drawer drag to rearrange position and swipe to remove 2022-08-05 20:27:43 +08:00
tarsin 2781aaca14 Make gallerylist qs drawer drag to rearrange position and swipe to remove 2022-08-05 20:03:51 +08:00
tarsin 91b9c9af40 Reimplement history swipe-to-remove 2022-08-05 19:44:12 +08:00
tarsin 5c0d10b41a Clean up readme 2022-08-05 18:14:15 +08:00
tarsin a3bdbd7a85 Drop android-advancedrecyclerview
* Actually this library is unmaintained for above a year, and drag feature we used is totally broken on Android 12 and above https://github.com/h6ah4i/android-advancedrecyclerview/issues/516 https://github.com/Ehviewer-Overhauled/Ehviewer/issues/54
2022-08-05 18:12:23 +08:00
tarsin f66151f880 Clean up proguards 2022-08-05 15:13:59 +08:00
txyyh 6c5f52995d Add mult-language README and markdown TOC (#165)
* Add mult-language README

* Add markdown TOC

* Add language selection to README

* Fix wrong branch name in README
2022-08-05 14:55:03 +08:00
tarsin d94b96a8eb Clean up 2022-08-04 13:05:47 +08:00
tarsin a5d90cc97d Make release
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-08-04 07:39:53 +08:00
tarsin 657d566e89 No need to merge old local db 2022-08-04 07:06:01 +08:00
tarsin 5e6d24f01e Clean up 2022-08-04 06:49:37 +08:00
tarsin e353bbe4c3 Refine database import export 2022-08-04 06:35:42 +08:00
renovate[bot] 4ee2cdf19c Update plugin com.android.application to v7.2.2 (1.8.x.x) (#162)
Update plugin com.android.application to v7.2.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-04 04:59:09 +08:00
txyyh b28831a8aa Delete useless string (#161) 2022-08-04 04:58:53 +08:00
EdwardWuandtarsin 800b64d5d3 fixup! Ehviewer: Add Chinese(Traditional) Translate (#160)
* Ehviewer: Add Chinese(Traditional) Translate

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>

* fixup! Ehviewer: Add Chinese(Traditional) Translate

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>

Co-authored-by: tarsin <43900799+asuka-mio@users.noreply.github.com>
2022-08-03 22:22:08 +08:00
tarsin cfa6164b5c Use ksp 2022-08-03 22:16:01 +08:00
tarsin 9cb22b6e17 Initial stub migrate to androidx room 2022-08-03 21:59:55 +08:00
tarsin 19891e6768 Fix location 2022-08-03 20:39:11 +08:00
tarsin c616410e0b Drop daogenerator first 2022-08-03 20:33:45 +08:00
EdwardWu 31e1e79ed2 Ehviewer: Add Chinese(Traditional) Translate (#158)
Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-08-03 19:48:22 +08:00
tarsin 6191a9827d Use Ofast 2022-08-03 19:14:11 +08:00
tarsin f373d8f293 Allow universal apk build 2022-08-03 19:07:08 +08:00
tarsin c233eb5a30 Avoid unnecessary native method calling java method 2022-08-03 18:49:00 +08:00
tarsin 360a3e9b5f Clean up 2022-08-03 18:10:30 +08:00
tarsin 01aff59d46 Re-checkout 2022-08-03 17:56:27 +08:00
tarsin f0f1dca7cb Fix build 2022-08-03 17:51:01 +08:00
tarsin 6f3bd92539 Merge native 2022-08-03 17:48:46 +08:00
tarsin a8023f7334 Minify proguard rules 2022-08-03 17:34:21 +08:00
txyyh 8c368972ec Add get-it-on-fdroid into README (#157)
* Fix wrong alt

* Add get-it-on-fdroid
2022-08-02 23:52:50 +08:00
tarsin 5aea90314a Update README.md 2022-08-02 14:02:53 +08:00
tarsin 0c8ba49d61 Update README.md 2022-08-02 14:00:40 +08:00
tarsin ea5b8b9f81 Update README.md 2022-08-02 13:52:28 +08:00
LaLucid 6b21f5082c update CI (#155)
* update CI

* do not need setup-android
2022-08-02 13:46:49 +08:00
tarsin 438d7f1b34 Use O3 optimization 2022-08-01 22:24:01 +08:00
tarsin 301797b477 Import androidx navigation 2022-08-01 10:18:36 +08:00
tarsin 2758be2462 Code clean 2022-08-01 10:14:33 +08:00
tarsin 8f03e4910d Code clean 2022-08-01 10:04:27 +08:00
tarsin a09efd3eea Fix #81 2022-08-01 09:48:56 +08:00
tarsin a66d460572 Remove warning 2022-08-01 09:35:17 +08:00
txyyh 162b88915e 1.8.0.4
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-31 23:10:42 +08:00
txyyh d014b6947c Rewrite download section in README (#150)
* Delete CI Download in README

* Add Chinese NOTE

* Add English NOTE

* Use icon for github download
2022-07-31 12:34:48 +08:00
tarsin a6188964c9 Add simple sanitize, Fix#148 2022-07-31 09:23:41 +08:00
tarsin 722fbec72c 1.8.0.3
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-29 18:08:31 +08:00
tarsin 22190fb686 Fix a double-free problem 2022-07-28 23:45:35 +08:00
txyyh 49ea70c839 Add monochrome icon (#144) 2022-07-28 12:06:56 +08:00
tarsin c655e7e1c9 Drop blank res 2022-07-28 11:28:45 +08:00
tarsin d13cd1e07f 1.8.0.2
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-28 10:37:07 +08:00
tarsin 7365fd0448 Fix a memleak 2022-07-28 10:33:47 +08:00
txyyh f1febca312 Let renovate bot detect two branch 2022-07-28 04:12:33 +08:00
renovate[bot] dbf67404c0 Update fragment_version to v1.5.1 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-28 03:39:42 +08:00
renovate[bot] 8cfe8c4b80 Update collection_version to v1.3.0-alpha02 (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-28 03:39:25 +08:00
tarsin 674f53315f Code clean 2022-07-27 19:46:39 +08:00
tarsin e51cd6b90d Drop taptargetview
* Not MD3
2022-07-25 20:44:59 +08:00
tarsin a44f12b2e0 Reformat Code 2022-07-25 20:07:16 +08:00
tarsin a2552ed5b9 Reformat 2022-07-25 20:05:38 +08:00
tarsin 763c007950 Log tweaks 2022-07-25 20:02:47 +08:00
tarsin b221bf8e52 Release 1.8.0.1
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-25 19:16:07 +08:00
tarsin ab9a89685c Code clean 2022-07-25 19:02:10 +08:00
tarsin 41ccb16227 Code clean 2022-07-25 18:42:18 +08:00
tarsin f6bf7b1ba6 No need to thanks image decs anymore 2022-07-25 13:06:24 +08:00
tarsin 09e7113df1 Fix a dirty dealloc 2022-07-25 12:23:52 +08:00
tarsin 62a7e283a4 Should be able to support all format now 2022-07-25 10:51:57 +08:00
366 changed files with 10164 additions and 17747 deletions

No files matched your search

+26 -4
View File
@@ -1,8 +1,17 @@
name: Bug 反馈 / Bug report
description: 提交一个问题报告 / Create a bug report to help us improve
description: 提交一个问题报告 / Create a bug report
labels:
- "bug"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交问题报告前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before create bug report
- type: textarea
id: reproduce
attributes:
@@ -44,12 +53,25 @@ body:
validations:
required: true
- type: textarea
id: more
attributes:
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
placeholder: |
其他有用的信息与附件
Additional details and attachments
validations:
required: false
- type: input
id: version
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 四位版本号 / 4 digits version code
placeholder: 1.6.9.13
validations:
required: true
@@ -57,7 +79,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
@@ -119,4 +141,4 @@ body:
required: true
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
required: true
required: true
+1
View File
@@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
+14 -5
View File
@@ -1,8 +1,17 @@
name: 功能请求 / Feature request
description: 提出一个功能建议 / Suggest an idea for this project
description: 提出一个功能建议 / Suggest an idea
labels:
- "enhancement"
body:
- type: markdown
attributes:
value: |
感谢您愿意为 Ehviewer-Overhauled 做出贡献!
提交功能建议前,还请首先完成文末的自查步骤
Thanks for your contribution on Ehviewer-Overhauled Project!
Please finish verify steps which list in the end first before suggest an idea
- type: textarea
id: request
attributes:
@@ -33,7 +42,7 @@ body:
- type: textarea
id: more
attributes:
label: 其他细节 / Addition details
label: 备注 / Addition details
description: |
在此处写下其他您想说的内容
Describe addition details here
@@ -48,7 +57,7 @@ body:
attributes:
label: EhViewer 版本号 / EhViewer version code
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
placeholder: 四位版本号 / 4 digits version code
placeholder: 1.6.9.13
validations:
required: true
@@ -56,7 +65,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
@@ -82,4 +91,4 @@ body:
required: true
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
required: true
required: true
+14 -10
View File
@@ -1,6 +1,11 @@
name: CI
on: [push]
on:
push:
branches:
- '*'
tags-ignore:
- "*"
jobs:
linux:
@@ -9,25 +14,24 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
- run: |
git submodule update --init --recursive
gradle assembleRelease
with:
gradle-home-cache-cleanup: true
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
+7 -9
View File
@@ -9,25 +9,23 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
- run: |
git submodule update --init --recursive
gradle assembleRelease
with:
arguments: assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
+41 -14
View File
@@ -12,33 +12,60 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
- name: Setup Java 19
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
java-version: 19
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.1.8937393" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
- run: |
git submodule update --init --recursive
gradle assembleRelease
with:
arguments: assembleRelease
- name: Rename Apks
run: |
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
- name: Releases
uses: softprops/action-gh-release@v0.1.14
uses: softprops/action-gh-release@v1
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/app-arm64-v8a-release.apk
app/build/outputs/apk/release/app-x86_64-release.apk
app/build/outputs/apk/release/app-x86-release.apk
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
app/build/outputs/apk/release/EhViewer-universal.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a.apk
app/build/outputs/apk/release/EhViewer-x86_64.apk
app/build/outputs/apk/release/EhViewer-x86.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a.apk
- name: Collect Apks
run: |
mkdir app/build/outputs/apk/release/fdroid
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: cpina/github-action-push-to-another-repository@v1.5
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: app/build/outputs/apk/release/fdroid
destination-github-username: Ehviewer-Overhauled
destination-repository-name: fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
+6 -6
View File
@@ -1,9 +1,9 @@
[submodule "native/src/main/jni/libarchive"]
path = native/src/main/jni/libarchive
[submodule "app/src/main/cpp/libarchive"]
path = app/src/main/cpp/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "native/src/main/jni/liblzma"]
path = native/src/main/jni/liblzma
[submodule "app/src/main/cpp/liblzma"]
path = app/src/main/cpp/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "native/src/main/jni/nettle/nettle"]
path = native/src/main/jni/nettle/nettle
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+45 -22
View File
@@ -1,5 +1,17 @@
<p align="right">
<strong>English<strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="art/launcher_icon-web.png" width="200">
<img src="art/launcher_icon-web.svg" width="200">
<br>EhViewer<br>
</h1>
@@ -7,7 +19,7 @@
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
@@ -21,22 +33,40 @@
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
License
</a>
</h3>
</div>
# Description
致力于轻量化与良好性能表现的 EhViewer 分支
已针对性适配 Material Design 3 及其动态取色系统
EhViewer fork dedicated to lightweight and high-performance
with Material Design 3 and Dynamic Color Support
# Download
[Github 下载](https://github.com/Ehviewer-Overhauled/Ehviewer/releases)
[CI 版本下载(需登录)](https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml)
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="art/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="art/get-it-on-fdroid.svg" width="200px"/></a>
# Screenshot
@@ -45,24 +75,17 @@ with Material Design 3 and Dynamic Color Support
# Thanks
本项目受到了诸多开源项目的帮助
Here is the libraries
- [AOSP](http://source.android.com/)
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
- [AOSP & AndroidX](http://source.android.com/)
- [Coil](https://coil-kt.github.io/coil/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [GnuTLS](https://gnutls.org/)
- [material-components-android](https://github.com/material-components/material-components-android)
- [material-design-icons](https://github.com/google/material-design-icons)
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
- [apng](http://apng.sourceforge.net/)
- [giflib](http://giflib.sourceforge.net)
- [greenDAO](https://github.com/greenrobot/greenDAO)
- [jsoup](https://github.com/jhy/jsoup)
- [libjpeg-turbo](https://libjpeg-turbo.org/)
- [libpng](http://www.libpng.org/pub/png/libpng.html)
- [okhttp](https://github.com/square/okhttp)
- [roaster](https://github.com/forge/roaster)
- [TapTargetView](https://github.com/KeepSafe/TapTargetView)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
- [libarchive](http://www.libarchive.org/)
- [XZ Utils](https://tukaani.org/xz/)
+2 -1
View File
@@ -4,4 +4,5 @@ manifest-merger-release-report.txt
/src/main/libs
/src/main/obj
# Intellij
*.iml
*.iml
/.cxx
+80 -43
View File
@@ -1,23 +1,20 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
Properties localProperties = new Properties()
if (project.rootProject.file('local.properties').exists()) {
localProperties.load(project.rootProject.file('local.properties').newDataInputStream())
}
buildToolsVersion "33.0.1"
ndkVersion "25.1.8937393"
splits {
abi {
enable = true
reset()
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
universalApk = true
}
}
@@ -32,20 +29,36 @@ android {
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 31
minSdkVersion 28
targetSdkVersion 33
versionCode 180000
versionName "1.8.0.0"
versionCode 180026
versionName "1.8.4.0-alpha02"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_19
targetCompatibility JavaVersion.VERSION_19
}
kotlinOptions {
jvmTarget = '11'
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
// Generate smaller bytecode by not generating runtime not-null assertions.
"-Xno-call-assertions",
"-Xno-param-assertions",
"-Xno-receiver-assertions",
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
]
}
lint {
@@ -78,56 +91,64 @@ android {
}
}
sourceSets {
main {
java.srcDirs += 'src/main/java-gen'
}
buildFeatures {
buildConfig true
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
}
namespace 'com.hippo.ehviewer'
}
tasks.withType(JavaCompile) {
task -> task.dependsOn ":daogenerator:executeDaoGenerator"
}
dependencies {
def activity_version = "1.6.0-alpha05"
def activity_version = "1.6.1"
implementation "androidx.activity:activity:$activity_version"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.annotation:annotation-experimental:1.2.0"
implementation "androidx.appcompat:appcompat:1.6.0-alpha05"
implementation "androidx.annotation:annotation:1.5.0"
implementation "androidx.annotation:annotation-experimental:1.3.0"
implementation "androidx.appcompat:appcompat:1.6.0-rc01"
def arch_core_version = "2.1.0"
implementation "androidx.arch.core:core-common:$arch_core_version"
implementation "androidx.arch.core:core-runtime:$arch_core_version"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
def collection_version = "1.3.0-alpha01"
def collection_version = "1.3.0-alpha02"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
def compose_version = "1.3.1"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-saveable:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.0.1"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
def core_version = "1.9.0-alpha05"
def core_version = "1.9.0"
implementation "androidx.core:core:$core_version"
implementation "androidx.core:core-ktx:$core_version"
implementation "androidx.customview:customview:1.2.0-alpha01"
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
implementation "androidx.customview:customview:1.2.0-alpha02"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.5.0"
def fragment_version = "1.6.0-alpha04"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
def lifecycle_version = "2.6.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
def lifecycle_version = "2.6.0-alpha03"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
@@ -135,8 +156,23 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.paging:paging-runtime:3.2.0-alpha03"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
def room_version = "2.5.0-rc01"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation "androidx.transition:transition-ktx:1.4.1"
@@ -144,24 +180,25 @@ dependencies {
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.5.0-beta01'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.7.0-alpha03'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
implementation 'com.google.android.material:material:1.8.0-SNAPSHOT'
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'org.greenrobot:greendao:3.3.0'
implementation 'org.jsoup:jsoup:1.15.2'
implementation projects.native
implementation 'io.coil-kt:coil:2.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'androidx.compose.material'
}
+6 -19
View File
@@ -7,28 +7,15 @@
public static void throw*(...);
}
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keep class com.hippo.ehviewer.dao.* { *; }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
-mergeinterfacesaggressively
-overloadaggressively
-dontwarn net.sqlcipher.database.**
-dontwarn rx.**
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn net.sqlcipher.Cursor
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
+14 -5
View File
@@ -23,14 +23,15 @@
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Required to maintain app compatibility. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
@@ -40,11 +41,13 @@
android:allowBackup="true"
android:appCategory="image"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_scheme"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
@@ -144,6 +147,12 @@
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
@@ -1,17 +1,17 @@
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Oz -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -flto\
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
-mllvm -polly-run-inliner \
-mllvm -polly-isl-arg=--no-schedule-serialize-sccs \
-mllvm -polly-ast-use-context \
-mllvm -polly-ast-use-context \
-mllvm -polly-detect-keep-going \
-mllvm -polly-position=before-vectorizer \
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
-mllvm -polly-invariant-load-hoisting")
# Build GNUTLS libnettle
add_subdirectory(nettle)
@@ -44,10 +44,7 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
# Build libarchive
add_subdirectory(libarchive)
# Build hippo's image lib
add_subdirectory(image)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED main.c archive.c)
target_include_directories(${PROJECT_NAME} PUBLIC image/image libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} image archive_static -Wl,--gc-sections)
add_library(${PROJECT_NAME} SHARED archive.c image.c fdutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv2 -Wl,--gc-sections)
+368
View File
@@ -0,0 +1,368 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <syscall.h>
#include <jni.h>
#include <android/log.h>
#include <archive.h>
#include <archive_entry.h>
#define LOG_TAG "libarchive_wrapper"
#include "ehviewer.h"
typedef struct {
int using;
int next_index;
struct archive *arc;
struct archive_entry *entry;
} archive_ctx;
pthread_mutex_t ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
static size_t archiveSize = 0;
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
const char supportExt[9][6] = {
"jpeg",
"jpg",
"png",
"gif",
"webp",
"bmp",
"ico",
"wbmp",
"heif"
};
static inline int filename_is_playable_file(const char *name) {
const char *dotptr = strrchr(name, '.');
if (!dotptr++)
return false;
int i;
for (i = 0; i < 9; i++)
if (strcmp(dotptr, supportExt[i]) == 0)
return true;
return false;
}
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
count++;
if (!count)
LOGE("%s", archive_error_string(ctx->arc));
return count;
}
static void archive_release_ctx(archive_ctx *ctx) {
if (ctx) {
archive_read_close(ctx->arc);
archive_read_free(ctx->arc);
free(ctx);
}
}
static int archive_alloc_ctx(archive_ctx **ctxptr) {
archive_ctx *ctx;
int err;
ctx = calloc(1, sizeof(archive_ctx));
if (!ctx)
return -ENOMEM;
ctx->arc = archive_read_new();
ctx->using = 1;
if (!ctx->arc) {
free(ctx);
return -ENOMEM;
}
archive_read_support_format_tar(ctx->arc);
archive_read_support_format_7zip(ctx->arc);
archive_read_support_format_rar5(ctx->arc);
archive_read_support_format_zip(ctx->arc);
archive_read_support_filter_gzip(ctx->arc);
archive_read_support_filter_xz(ctx->arc);
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(ctx->arc, passwd);
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err) {
archive_read_free(ctx->arc);
free(ctx);
return err;
}
*ctxptr = ctx;
return 0;
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
assert(!(ctx->next_index > index));
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
continue;
if (ctx->next_index++ == index) {
return ctx->next_index - 1;
}
}
return ARCHIVE_FATAL;
}
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
if (ctx_pool[i]->using)
continue;
if (ctx_pool[i]->next_index > idx)
continue;
if (!ctx || ctx_pool[i]->next_index > ctx->next_index)
ctx = ctx_pool[i];
if (ctx->next_index == idx)
break;
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&ctx_lock);
if (!ctx) {
archive_ctx *victimCtx = NULL;
int victimIdx = 0;
int replace = 1;
ret = archive_alloc_ctx(&ctx);
if (ret)
return ret;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
replace = 0;
break;
}
if (ctx_pool[i]->using)
continue;
if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {
victimCtx = ctx_pool[i];
victimIdx = i;
}
}
if (replace) {
archive_release_ctx(victimCtx);
ctx_pool[victimIdx] = ctx;
}
pthread_mutex_unlock(&ctx_lock);
}
ret = archive_skip_to_index(ctx, idx);
if (ret != idx) {
ret = archive_errno(ctx->arc);
LOGE("Skip to index failed:%s", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return ret;
}
*ctxptr = ctx;
return 0;
}
JNIEXPORT jint JNICALL
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int mmap_flags = MAP_PRIVATE;
if (size <= POPULATE_THRESHOLD)
mmap_flags |= MAP_POPULATE;
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
return 0;
}
archiveSize = size;
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
pthread_mutex_init(&ctx_lock, 0);
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
LOGE("Allocate archive ctx pool failed:ENOMEM");
return 0;
}
long r = archive_alloc_ctx(&ctx);
if (r) {
if (r == -ENOMEM)
LOGE("%s", "Mem alloc failed");
r = 0;
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
r = archive_list_all_entries(ctx);
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
break;
case 0: // format supports but no encrypted entry found
default:
need_encrypt = false;
}
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR_V5:
madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);
break;
case ARCHIVE_FORMAT_7ZIP: // Seek is bad
madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);
break;
default:;
}
}
archive_release_ctx(ctx);
return r;
}
JNIEXPORT jobject JNICALL
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size);
if (!buffer) {
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
}
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(buffer);
return 0;
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(passwd);
free(ctx_pool);
pthread_mutex_destroy(&ctx_lock);
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
EH_UNUSED(thiz);
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
EH_UNUSED(thiz);
struct archive_entry *entry;
archive_ctx *ctx;
jboolean ret = true;
int len = (*env)->GetStringUTFLength(env, str);
passwd = realloc(passwd, len * sizeof(char));
if (!passwd) {
LOGE("Allocate passwd buffer failed");
return false;
}
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
archive_alloc_ctx(&ctx);
void *tmpBuf = alloca(4096);
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(entry)))
continue;
if (!archive_entry_is_encrypted(entry))
continue;
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
ret = false;
}
break;
}
archive_release_ctx(ctx);
return ret;
}
JNIEXPORT jstring JNICALL
Java_com_hippo_UriArchiveAccessor_getFilename(JNIEnv *env, jobject thiz, jint index) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return NULL;
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
ctx->using = 0;
return str;
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_extractToFd(JNIEnv *env, jobject thiz, jint index, jint fd) {
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (!ret) {
archive_read_data_into_fd(ctx->arc, fd);
ctx->using = 0;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
free(addr);
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EHVIEWER_EHVIEWER_H
#define EHVIEWER_EHVIEWER_H
#define EH_UNUSED(x) (void)x
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
#define madvise_log_if_error(addr, len, advice) \
if (madvise(addr, len, advice)) \
LOGE("%s%p%s%zu%s%d%s%s%s", "madvise addr:", addr, "len:", len, "with advice ", advice, " failed with error: ", strerror(errno), ", Ignored")
#endif /* EHVIEWER_EHVIEWER_H */
+16
View File
@@ -0,0 +1,16 @@
#include <jni.h>
JNIEXPORT jint JNICALL
Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
jint fd = -1;
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
if (fdClass != NULL) {
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
}
}
return fd;
}
+143
View File
@@ -0,0 +1,143 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <string.h>
#include <android/bitmap.h>
#include <android/data_space.h>
#include <android/log.h>
#include <GLES2/gl2.h>
#include <jni.h>
#define TAG "ImageDecoder_wrapper"
#include "ehviewer.h"
#define IMAGE_TILE_MAX_SIZE (512 * 512)
static char tile_buffer[IMAGE_TILE_MAX_SIZE * 4];
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
int left;
int line;
size_t line_stride;
int src_stride;
int src_pos;
int dst_pos;
size_t dst_blank_length;
// Sanitize
if (src_x < 0) {
width -= src_x;
dst_x -= src_x;
src_x = 0;
}
if (dst_x < 0) {
width -= dst_x;
src_x -= dst_x;
dst_x = 0;
}
if (width <= 0) {
return false;
}
if (src_y < 0) {
height -= src_y;
dst_y -= src_y;
src_y = 0;
}
if (dst_y < 0) {
height -= dst_y;
src_y -= dst_y;
dst_y = 0;
}
if (height <= 0) {
return false;
}
left = src_x + width - src_w;
if (left > 0) {
width -= left;
}
left = dst_x + width - dst_w;
if (left > 0) {
width -= left;
}
if (width <= 0) {
return false;
}
left = src_y + height - src_h;
if (left > 0) {
height -= left;
}
left = dst_y + height - dst_h;
if (left > 0) {
height -= left;
}
if (height <= 0) {
return false;
}
// Init
line_stride = (size_t) (width * 4);
src_stride = src_w * 4;
src_pos = src_y * src_stride + src_x * 4;
dst_pos = 0;
dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * 4;
// First line
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
// Other lines
dst_blank_length = (size_t) ((dst_w - width) * 4);
for (line = 1; line < height; line++) {
dst_pos += (int) dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int) line_stride;
src_pos += src_stride;
}
return true;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init,
jint offset_x, jint offset_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_lockPixels(env, bitmap, &pixels);
AndroidBitmap_getInfo(env, bitmap, &info);
copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height);
AndroidBitmap_unlockPixels(env, bitmap);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
}
}
@@ -149,7 +149,7 @@
#define PACKAGE_NAME "nettle"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "nettle 3.8"
#define PACKAGE_STRING "nettle 3.8.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "nettle"
@@ -158,7 +158,7 @@
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "3.8"
#define PACKAGE_VERSION "3.8.1"
/* The size of `long', as computed by sizeof. */
#define SIZEOF_LONG __SIZEOF_LONG__
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -15,20 +15,15 @@
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo
package com.hippo;
import java.io.FileDescriptor
public class Native {
public static void initialize() {
System.loadLibrary("ehviewer");
object Native {
fun initialize() {
System.loadLibrary("ehviewer")
}
public static native String getlibarchiveVersion();
public static native String getliblzmaVersion();
public static native String getzlibVersion();
public static native String getnettleVersion();
}
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
}
@@ -0,0 +1,43 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo
import android.content.Context
import android.net.Uri
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
fun open(): Int {
return openArchive(pfd.fd, pfd.statSize)
}
private external fun openArchive(fd: Int, size: Long): Int
external fun extractToByteBuffer(index: Int): ByteBuffer?
external fun releaseByteBuffer(buffer: ByteBuffer)
external fun extractToFd(index: Int, fd: Int)
external fun getFilename(index: Int): String
external fun needPassword(): Boolean
external fun providePassword(str: String): Boolean
private external fun closeArchive()
fun close() {
closeArchive()
pfd.close()
}
}
@@ -1,28 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
public @interface Implemented {
Class value();
}
@@ -0,0 +1,8 @@
package com.hippo.app
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
open class BaseDialogBuilder constructor(
context: Context
) : MaterialAlertDialogBuilder(context)
@@ -18,7 +18,6 @@ package com.hippo.app
import android.content.Context
import android.view.LayoutInflater
import android.widget.CheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.ehviewer.R
class CheckBoxDialogBuilder constructor(
@@ -26,7 +25,7 @@ class CheckBoxDialogBuilder constructor(
message: String?,
checkText: String?,
checked: Boolean
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
) {
private val mCheckBox: CheckBox
@@ -23,7 +23,6 @@ import android.widget.EditText
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.hippo.ehviewer.R
@@ -31,7 +30,7 @@ class EditTextDialogBuilder constructor(
context: Context,
text: String?,
hint: String?
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
), OnEditorActionListener {
private val mTextInputLayout: TextInputLayout
@@ -0,0 +1,14 @@
package com.hippo.app
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import com.google.android.material.slider.Slider
class HapticSlider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Slider(context, attrs) {
init {
addOnChangeListener { _, _, _ -> performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) }
}
}
@@ -23,14 +23,13 @@ import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.ehviewer.R
import com.hippo.yorozuya.ViewUtils
class ListCheckBoxDialogBuilder constructor(
context: Context, items: Array<CharSequence>,
listener: OnItemClickListener?, checkText: String?, checked: Boolean
) : MaterialAlertDialogBuilder(
) : BaseDialogBuilder(
context
) {
private val mCheckBox: CheckBox
@@ -1,493 +0,0 @@
/*
* Copyright (C) 2015-2016 Hippo Seven
*
* 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.hippo.beerbelly;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public abstract class BeerBelly<V> {
private static final String TAG = BeerBelly.class.getSimpleName();
private final boolean mHasMemoryCache;
private final boolean mHasDiskCache;
@Nullable
private MemoryCache<V> mMemoryCache;
@Nullable
private DiskCache<V> mDiskCache;
public BeerBelly(BeerBellyParams params) {
params.isValid();
mHasMemoryCache = params.hasMemoryCache;
mHasDiskCache = params.hasDiskCache;
if (mHasMemoryCache) {
initMemoryCache(params.memoryCacheMaxSize);
}
if (mHasDiskCache) {
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
}
}
private void initMemoryCache(int maxSize) {
mMemoryCache = new MemoryCache<>(maxSize, this);
}
private void initDiskCache(File cacheDir, int maxSize) {
// Set up disk cache
try {
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
} catch (IOException e) {
Log.e(TAG, "Can't create disk cache", e);
}
}
protected abstract int sizeOf(String key, V value);
protected abstract void memoryEntryAdded(V value);
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
protected abstract V read(@NonNull InputStreamPipe isPipe);
protected abstract boolean write(OutputStream os, V value);
@Nullable
public LruCache<String, V> getMemoryCache() {
return mMemoryCache;
}
@Nullable
public SimpleDiskCache getDiskCache() {
if (mDiskCache != null) {
return mDiskCache.mDiskCache;
} else {
return null;
}
}
/**
* Check if have memory cache
*
* @return true if have memory cache
*/
public boolean hasMemoryCache() {
return mHasMemoryCache;
}
/**
* Check if have disk cache
*
* @return true if have disk cache
*/
public boolean hasDiskCache() {
return mHasDiskCache;
}
/**
* Get memory cache usage size
*
* @return {@code -1} for no memory cache
*/
public int memorySize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.size();
} else {
return -1;
}
}
/**
* Get memory cache max size
*
* @return {@code -1} for no memory cache
*/
public int memoryMaxSize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.maxSize();
} else {
return -1;
}
}
/**
* Get disk cache usage size
*
* @return {@code -1L} for no memory cache
*/
public long diskSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.size();
} else {
return -1L;
}
}
/**
* Get disk cache max size
*
* @return {@code -1L} for no memory cache
*/
public long diskMaxSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.getMaxSize();
} else {
return -1L;
}
}
/**
* Get value from memory cache
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache
*/
public V getFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.get(key);
} else {
return null;
}
}
/**
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache or get error
*/
public V getFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.get(key);
} else {
return null;
}
}
/**
* Get value from memory cache and disk cache. If miss in memory cache and
* get in disk cache, it will put value from disk cache to memory cache.
*
* @param key the key to get value
* @return the value you get
*/
public V get(@NonNull String key) {
V value = getFromMemory(key);
if (value != null) {
// Get it in memory cache
return value;
}
value = getFromDisk(key);
if (value != null) {
// Get it in disk cache
putToMemory(key, value);
return value;
}
return null;
}
/**
* Put value to memory cache
*
* @param key the key
* @param value the value
* @return false if no memory cache
*/
public boolean putToMemory(@NonNull String key, @NonNull V value) {
if (mHasMemoryCache && mMemoryCache != null) {
memoryEntryAdded(value);
mMemoryCache.put(key, value);
return true;
} else {
return false;
}
}
/**
* Put value to disk cache
*
* @param key the key
* @param value the value
* @return false if no disk cache or get error
*/
public boolean putToDisk(@NonNull String key, @NonNull V value) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.put(key, value);
} else {
return false;
}
}
/**
* Put value to memory cache and disk cache
*
* @param key the key
* @param value the value
*/
public void put(@NonNull String key, @NonNull V value) {
putToMemory(key, value);
putToDisk(key, value);
}
public void removeFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.remove(key);
}
}
public void removeFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.remove(key);
}
}
public void remove(@NonNull String key) {
removeFromMemory(key);
removeFromDisk(key);
}
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.pull(key, os);
} else {
return false;
}
}
/**
* @param key the key
* @param is the input stream to store
* @return false if no disk cache or get error
*/
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.putRaw(key, is);
} else {
return false;
}
}
/**
* Evicts all of the items from the memory cache
*/
public void clearMemory() {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
/**
* Clear disk cache
*/
public void clearDisk() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.clear();
}
}
public void flush() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.flush();
}
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void clear() {
clearMemory();
clearDisk();
}
/**
* A holder class that contains cache parameters.
*/
public static class BeerBellyParams {
/**
* is memory cache available
*/
public boolean hasMemoryCache = false;
/**
* the maximum number of bytes the memory cache should use to store
*/
public int memoryCacheMaxSize = 0;
/**
* is disk cache available
*/
public boolean hasDiskCache = false;
/**
* the dir to store disk cache
*/
public File diskCacheDir = null;
/**
* the maximum number of bytes the disk cache should use to store
*/
public int diskCacheMaxSize = 0;
/**
* Check BeerBellyParams is valid
*/
public void isValid() throws IllegalStateException {
if (!hasMemoryCache && !hasDiskCache) {
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
}
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
}
if (hasDiskCache) {
if (diskCacheDir == null) {
throw new IllegalStateException("Disk cache dir can't be null.");
}
if (diskCacheMaxSize <= 0) {
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
}
}
}
}
private static class DiskCache<E> {
private static final int IO_BUFFER_SIZE = 8 * 1024;
private final SimpleDiskCache mDiskCache;
private final BeerBelly<E> mParent;
private final File mCacheDir;
private final int mMaxSize;
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
mDiskCache = new SimpleDiskCache(cacheDir, size);
mParent = parent;
mCacheDir = cacheDir;
mMaxSize = size;
}
public File getCacheDir() {
return mCacheDir;
}
public int getMaxSize() {
return mMaxSize;
}
public long size() {
return mDiskCache.size();
}
public void flush() {
mDiskCache.flush();
}
public void clear() {
mDiskCache.clear();
}
public E get(String key) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return null;
} else {
return mParent.read(isPipe);
}
}
public boolean put(String key, E value) {
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
BufferedOutputStream buffOut = null;
try {
osPipe.obtain();
OutputStream os = osPipe.open();
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
return mParent.write(os, value);
} catch (IOException e) {
return false;
} finally {
Util.closeQuietly(buffOut);
osPipe.close();
osPipe.release();
}
}
public boolean putRaw(String key, InputStream is) {
return mDiskCache.put(key, is);
}
public void remove(String key) {
mDiskCache.remove(key);
}
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return false;
} else {
try {
isPipe.obtain();
InputStream is = isPipe.open();
Util.copy(is, os);
return true;
} catch (IOException e) {
return false;
} finally {
isPipe.close();
isPipe.release();
}
}
}
}
private class MemoryCache<E> extends LruCache<String, E> {
public BeerBelly<E> mParent;
public MemoryCache(int maxSize, BeerBelly<E> parent) {
super(maxSize);
mParent = parent;
}
@Override
protected int sizeOf(String key, E value) {
return mParent.sizeOf(key, value);
}
@Override
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
}
}
}
@@ -167,6 +167,7 @@ public final class DiskLruCache implements Closeable {
* its sequence number is not equal to its entry's sequence number.
*/
private long nextSequenceNumber = 0;
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
@@ -175,21 +176,7 @@ public final class DiskLruCache implements Closeable {
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
}
/**
* Opens the cache in {@code directory}, creating a cache if none exists
@@ -263,7 +250,21 @@ public final class DiskLruCache implements Closeable {
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
@@ -23,7 +23,6 @@ import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
/**
@@ -140,7 +139,7 @@ class StrictLineReader implements Closeable {
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
String res = new String(buf, pos, lineEnd - pos, charset.name());
String res = new String(buf, pos, lineEnd - pos, charset);
pos = i + 1;
return res;
}
@@ -152,11 +151,7 @@ class StrictLineReader implements Closeable {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
try {
return new String(buf, 0, length, charset.name());
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e); // Since we control the charset this will never happen.
}
return new String(buf, 0, length, charset);
}
};
@@ -1,223 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* 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.hippo.conaco;
import android.os.Process;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.UiThread;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.yorozuya.thread.PriorityThreadFactory;
import com.hippo.yorozuya.thread.SerialThreadExecutor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
@SuppressWarnings("rawtypes")
public final class Conaco<V> {
public static final int SOURCE_MEMORY = 0;
public static final int SOURCE_DISK = 1;
public static final int SOURCE_NETWORK = 2;
private static final String TAG = Conaco.class.getSimpleName();
private final ValueHelper<V> mHelper;
private final ValueCache<V> mCache;
private final OkHttpClient mOkHttpClient;
private final Register<V> mRegister;
private final SerialThreadExecutor mDiskExecutor;
private final ThreadPoolExecutor mNetworkExecutor;
private final AtomicInteger mIdGenerator;
private final boolean mDebug;
private Conaco(Builder<V> builder) {
mHelper = builder.objectHelper;
BeerBelly.BeerBellyParams beerBellyParams = new BeerBelly.BeerBellyParams();
beerBellyParams.hasMemoryCache = builder.hasMemoryCache;
beerBellyParams.memoryCacheMaxSize = builder.memoryCacheMaxSize;
beerBellyParams.hasDiskCache = builder.hasDiskCache;
beerBellyParams.diskCacheDir = builder.diskCacheDir;
beerBellyParams.diskCacheMaxSize = builder.diskCacheMaxSize;
mCache = new ValueCache<>(beerBellyParams, mHelper);
mOkHttpClient = builder.okHttpClient;
mRegister = new Register<>();
mDiskExecutor = new SerialThreadExecutor(3000L, new LinkedList<>(),
new PriorityThreadFactory("Conaco-Disk", Process.THREAD_PRIORITY_BACKGROUND));
mNetworkExecutor = new ThreadPoolExecutor(3, 3, 5L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new PriorityThreadFactory("Conaco-Network", Process.THREAD_PRIORITY_BACKGROUND));
mIdGenerator = new AtomicInteger();
mDebug = builder.debug;
}
public BeerBelly getBeerBelly() {
return mCache;
}
private void onUnregisterConacoTask(ConacoTask<V> task) {
ConacoTask<V> next = mRegister.getByKey(task.getKey());
if (next != null) {
startConacoTask(next);
}
}
private void startConacoTask(ConacoTask<V> task) {
Unikery<V> unikery = task.getUnikery();
// Can't unikery is gone, finish the task now
if (unikery == null) {
finishConacoTask(task);
return;
}
String key = task.getKey();
V value = null;
// Get from memory
if (key != null && task.useMemoryCache() && mHelper.useMemoryCache(key, null)) {
value = mCache.getFromMemory(key);
}
if (value == null || !unikery.onGetValue(value, SOURCE_MEMORY)) {
unikery.onMiss(SOURCE_MEMORY);
task.start();
} else {
// Get the object, finish the task
finishConacoTask(task);
}
}
@UiThread
public void load(ConacoTask.Builder<V> builder) {
builder.isValid();
if (mDebug) {
Log.d(TAG, "Key " + builder.getKey());
Log.d(TAG, "Url " + builder.getUrl());
}
Unikery<V> unikery = builder.getUnikery();
// Cancel first
cancel(unikery);
// Build conaco task
int id;
//noinspection StatementWithEmptyBody
while ((id = mIdGenerator.getAndIncrement()) == Unikery.INVALID_ID) ;
unikery.setTaskId(id);
builder.setId(id)
.setHelper(mHelper)
.setCache(mCache)
.setOkHttpClient(mOkHttpClient)
.setDiskExecutor(mDiskExecutor)
.setNetworkExecutor(mNetworkExecutor)
.setConaco(this);
ConacoTask<V> task = builder.build();
if (!mRegister.register(id, task)) {
startConacoTask(task);
} else {
// The key is repeated, wait
unikery.onWait();
}
}
@UiThread
public void cancel(Unikery<V> unikery) {
int id = unikery.getTaskId();
if (id != Unikery.INVALID_ID) {
unikery.setTaskId(Unikery.INVALID_ID);
ConacoTask<V> task = mRegister.unregister(id);
if (task != null) {
task.stop();
// Don't need unikery anymore
task.clearUnikery();
// Callback
onUnregisterConacoTask(task);
} else {
Log.e(TAG, "Can't find conaco task by id " + id);
}
}
}
public boolean isLoading(Unikery<V> unikery) {
int id = unikery.getTaskId();
return id != Unikery.INVALID_ID && mRegister.contain(id);
}
/**
* Unregister task, reset unikery id, clear unikery in task, call next
*/
void finishConacoTask(ConacoTask<V> task) {
mRegister.unregister(task.getId());
Unikery unikery = task.getUnikery();
if (unikery != null) {
unikery.setTaskId(Unikery.INVALID_ID);
}
task.clearUnikery();
onUnregisterConacoTask(task);
}
@IntDef({SOURCE_MEMORY, SOURCE_DISK, SOURCE_NETWORK})
@Retention(RetentionPolicy.SOURCE)
public @interface Source {
}
public static class Builder<T> extends BeerBelly.BeerBellyParams {
/**
* The client to get image from internet
*/
public OkHttpClient okHttpClient = null;
/**
* Decode, get size and others
*/
public ValueHelper<T> objectHelper = null;
public boolean debug = false;
@Override
public void isValid() throws IllegalStateException {
super.isValid();
if (okHttpClient == null) {
throw new IllegalStateException("No http client? How can I load image via url?");
}
}
public Conaco<T> build() {
isValid();
return new Conaco<>(this);
}
}
}
@@ -1,588 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* 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.hippo.conaco;
import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.okhttp.ChromeRequestBuilder;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@SuppressWarnings({"rawtypes", "deprecation"})
public final class ConacoTask<V> {
private static final String TAG = ConacoTask.class.getSimpleName();
private final int mId;
private final WeakReference<Unikery<V>> mUnikeryWeakReference;
private final String mKey;
private final String mUrl;
private final DataContainer mDataContainer;
private final boolean mUseMemoryCache;
private final boolean mUseDiskCache;
private final boolean mUseNetwork;
private final ValueHelper<V> mHelper;
private final ValueCache<V> mCache;
private final OkHttpClient mOkHttpClient;
private final Executor mDiskExecutor;
private final Executor mNetworkExecutor;
private final Conaco<V> mConaco;
private DiskLoadTask mDiskLoadTask;
private NetworkLoadTask mNetworkLoadTask;
private Call mCall;
private boolean mStart;
private volatile boolean mStop;
private ConacoTask(Builder<V> builder) {
mId = builder.mId;
mUnikeryWeakReference = new WeakReference<>(builder.mUnikery);
mKey = builder.mKey;
mUrl = builder.mUrl;
mDataContainer = builder.mDataContainer;
mUseMemoryCache = builder.mUseMemoryCache;
mUseDiskCache = builder.mUseDiskCache;
mUseNetwork = builder.mUseNetwork;
mHelper = builder.mHelper;
mCache = builder.mCache;
mOkHttpClient = builder.mOkHttpClient;
mDiskExecutor = builder.mDiskExecutor;
mNetworkExecutor = builder.mNetworkExecutor;
mConaco = builder.mConaco;
}
private static void putFromDiskCacheToDataContainer(String key, ValueCache cache, DataContainer container) {
SimpleDiskCache diskCache = cache.getDiskCache();
if (diskCache != null) {
InputStreamPipe pipe = diskCache.getInputStreamPipe(key);
if (pipe != null) {
try {
pipe.obtain();
container.save(pipe.open(), -1L, null, null);
} catch (IOException e) {
Log.d(TAG, "Can't save value from disk cache to data container");
e.printStackTrace();
container.remove();
} finally {
pipe.close();
pipe.release();
}
}
}
}
private static void putFromDataContainerToDiskCache(String key, ValueCache cache, DataContainer container) {
InputStreamPipe pipe = container.get();
if (pipe != null) {
try {
pipe.obtain();
cache.putRawToDisk(key, pipe.open());
} catch (IOException e) {
Log.w(TAG, "Can't save value from data container to disk cache", e);
cache.removeFromDisk(key);
} finally {
pipe.close();
pipe.release();
}
}
}
int getId() {
return mId;
}
String getKey() {
return mKey;
}
boolean useMemoryCache() {
return mUseMemoryCache;
}
@Nullable
Unikery<V> getUnikery() {
return mUnikeryWeakReference.get();
}
void clearUnikery() {
mUnikeryWeakReference.clear();
}
private void onFinish() {
if (!mStop) {
mConaco.finishConacoTask(this);
}/* else {
// It is done by Conaco
}*/
}
@UiThread
void start() {
if (mStop || mStart) {
return;
}
mStart = true;
Unikery unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
if (mUseDiskCache) {
mDiskLoadTask = new DiskLoadTask();
mDiskLoadTask.executeOnExecutor(mDiskExecutor);
return;
} else if (mUseNetwork) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onRequest();
mNetworkLoadTask = new NetworkLoadTask();
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
return;
} else {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onMiss(Conaco.SOURCE_NETWORK);
unikery.onFailure();
}
}
onFinish();
}
@UiThread
void stop() {
if (mStop) {
return;
}
mStop = true;
// Stop jobs
if (mDiskLoadTask != null) { // Getting from disk
mDiskLoadTask.cancel(false);
} else if (mNetworkLoadTask != null) { // Getting from network
mNetworkLoadTask.cancel(false);
if (mCall != null) {
mCall.cancel();
mCall = null;
}
}
Unikery unikery = mUnikeryWeakReference.get();
if (unikery != null) {
unikery.onCancel();
}
// Conaco handle the clean up
}
private boolean isNotNecessary(AsyncTask asyncTask) {
Unikery unikery = mUnikeryWeakReference.get();
return mStop || asyncTask.isCancelled() || unikery == null || unikery.getTaskId() != mId;
}
@SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
public static class Builder<T> {
private int mId;
private Unikery<T> mUnikery;
private String mKey;
private String mUrl;
private DataContainer mDataContainer;
private boolean mUseMemoryCache = true;
private boolean mUseDiskCache = true;
private boolean mUseNetwork = true;
private ValueHelper<T> mHelper;
private ValueCache<T> mCache;
private OkHttpClient mOkHttpClient;
private Executor mDiskExecutor;
private Executor mNetworkExecutor;
private Conaco<T> mConaco;
public Builder<T> setId(int id) {
mId = id;
return this;
}
public Unikery<T> getUnikery() {
return mUnikery;
}
public Builder<T> setUnikery(Unikery<T> unikery) {
mUnikery = unikery;
return this;
}
public String getKey() {
return mKey;
}
public Builder<T> setKey(String key) {
mKey = key;
return this;
}
public String getUrl() {
return mUrl;
}
public Builder<T> setUrl(String url) {
mUrl = url;
return this;
}
public Builder<T> setDataContainer(DataContainer dataContainer) {
mDataContainer = dataContainer;
return this;
}
boolean isUseMemoryCache() {
return mUseMemoryCache;
}
public Builder<T> setUseMemoryCache(boolean useMemoryCache) {
mUseMemoryCache = useMemoryCache;
return this;
}
boolean isUseDiskCache() {
return mUseDiskCache;
}
public Builder<T> setUseDiskCache(boolean useDiskCache) {
mUseDiskCache = useDiskCache;
return this;
}
boolean isUseNetwork() {
return mUseNetwork;
}
public Builder<T> setUseNetwork(boolean useNetwork) {
mUseNetwork = useNetwork;
return this;
}
ValueHelper<T> getHelper() {
return mHelper;
}
Builder<T> setHelper(ValueHelper<T> helper) {
mHelper = helper;
return this;
}
Builder<T> setCache(ValueCache<T> cache) {
mCache = cache;
return this;
}
Builder<T> setOkHttpClient(OkHttpClient okHttpClient) {
mOkHttpClient = okHttpClient;
return this;
}
Builder<T> setDiskExecutor(Executor diskExecutor) {
mDiskExecutor = diskExecutor;
return this;
}
Builder<T> setNetworkExecutor(Executor networkExecutor) {
mNetworkExecutor = networkExecutor;
return this;
}
Builder<T> setConaco(Conaco<T> conaco) {
mConaco = conaco;
return this;
}
public void isValid() {
if (mUnikery == null) {
throw new IllegalStateException("Must set unikery");
}
if (mKey == null && mUrl == null && mDataContainer == null) {
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
}
}
public ConacoTask<T> build() {
return new ConacoTask<>(this);
}
}
@SuppressLint("StaticFieldLeak")
private class DiskLoadTask extends AsyncTask<Void, Void, V> {
@Override
protected V doInBackground(Void... params) {
if (isNotNecessary(this)) {
return null;
} else {
V value = null;
// First check data container
if (mDataContainer != null && mDataContainer.isEnabled()) {
InputStreamPipe isp = mDataContainer.get();
if (isp != null) {
value = mHelper.decode(isp);
}
}
// Then check disk cache
if (mKey != null) {
if (value == null && mUseDiskCache) {
value = mCache.getFromDisk(mKey);
// Put back to data container
if (value != null && mDataContainer != null && mDataContainer.isEnabled()) {
putFromDiskCacheToDataContainer(mKey, mCache, mDataContainer);
}
}
if (value != null && mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
}
return value;
}
}
@Override
protected void onPostExecute(V value) {
mDiskLoadTask = null;
if (isCancelled() || mStop) {
onCancelled(value);
} else {
Unikery<V> unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
boolean getValue = false;
if ((value == null || !(getValue = unikery.onGetValue(value, Conaco.SOURCE_DISK))) && mUseNetwork) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onRequest();
mNetworkLoadTask = new NetworkLoadTask();
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
return;
} else if (!getValue) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onFailure();
}
}
onFinish();
}
}
@Override
protected void onCancelled(V holder) {
onFinish();
}
}
@SuppressLint("StaticFieldLeak")
private class NetworkLoadTask extends AsyncTask<Void, Long, V> implements ProgressNotifier {
@Override
public void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize) {
if (!isNotNecessary(this)) {
publishProgress(singleReceivedSize, receivedSize, totalSize);
}
}
private boolean putToDiskCache(InputStream is, long length) {
SimpleDiskCache diskCache = mCache.getDiskCache();
if (diskCache == null) {
return false;
}
OutputStreamPipe pipe = diskCache.getOutputStreamPipe(mKey);
try {
pipe.obtain();
OutputStream os = pipe.open();
final byte[] buffer = new byte[1024 * 4];
long receivedSize = 0;
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
receivedSize += bytesRead;
notifyProgress(bytesRead, receivedSize, length);
}
return true;
} catch (IOException e) {
return false;
} finally {
pipe.close();
pipe.release();
}
}
private boolean putToDataContainer(InputStream is, ResponseBody body) {
// Get media type
String mediaType;
MediaType mt = body.contentType();
if (mt != null) {
mediaType = mt.type() + '/' + mt.subtype();
} else {
mediaType = null;
}
return mDataContainer.save(is, body.contentLength(), mediaType, this);
}
@Override
protected V doInBackground(Void... params) {
if (isNotNecessary(this)) {
return null;
}
V value;
InputStream is = null;
try {
Log.d("TAG", "Conaco " + mUrl);
// Load it from internet
Request request = new ChromeRequestBuilder(mUrl).build();
mCall = mOkHttpClient.newCall(request);
Response response = mCall.execute();
ResponseBody body = response.body();
if (body == null) {
return null;
}
is = body.byteStream();
if (isNotNecessary(this)) {
return null;
}
if ((mDataContainer == null || !mDataContainer.isEnabled()) && mKey != null) {
if (putToDiskCache(is, body.contentLength())) {
// Get object from disk cache
value = mCache.getFromDisk(mKey);
if (value == null) {
// Maybe bad download, remove it from disk cache
mCache.removeFromDisk(mKey);
} else if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
return value;
} else {
// Maybe bad download, remove it from disk cache
mCache.removeFromDisk(mKey);
return null;
}
} else if (mDataContainer != null && mDataContainer.isEnabled()) {
// Check url Moved
HttpUrl requestHttpUrl = request.url();
HttpUrl responseHttpUrl = response.request().url();
if (!responseHttpUrl.equals(requestHttpUrl)) {
mDataContainer.onUrlMoved(mUrl, responseHttpUrl.url().toString());
}
// Put to data container
if (!putToDataContainer(is, body)) {
return null;
}
// Get value from data container
InputStreamPipe isp = mDataContainer.get();
if (isp == null) {
return null;
}
value = mHelper.decode(isp);
if (value == null) {
mDataContainer.remove();
} else if (mKey != null) {
// Put to disk cache
putFromDataContainerToDiskCache(mKey, mCache, mDataContainer);
if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
}
return value;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
mCall = null;
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
// Ignore
}
}
}
@Override
protected void onPostExecute(V holder) {
mNetworkLoadTask = null;
if (isCancelled() || mStop) {
onCancelled(holder);
} else {
Unikery<V> unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
if (holder == null || !unikery.onGetValue(holder, Conaco.SOURCE_NETWORK)) {
unikery.onFailure();
}
}
onFinish();
}
}
@Override
protected void onCancelled(V value) {
onFinish();
}
@Override
protected void onProgressUpdate(Long... values) {
Unikery unikery = mUnikeryWeakReference.get();
if (!mStop && !isCancelled() && unikery != null && unikery.getTaskId() == mId) {
unikery.onProgress(values[0], values[1], values[2]);
}
}
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* 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.hippo.conaco;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import java.io.InputStream;
public interface DataContainer {
/**
* Returns the enabled status for this DataContainer.
*/
boolean isEnabled();
/**
* Get 304 or something like that
*/
void onUrlMoved(String requestUrl, String responseUrl);
/**
* Save the {@code InputStream}
*/
boolean save(InputStream is, long length, @Nullable String mediaType, @Nullable ProgressNotifier notify);
/**
* Get {@code InputStreamPipe} for saved before
*/
@Nullable
InputStreamPipe get();
/**
* Remove saved stuff
*/
void remove();
}
@@ -1,72 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.conaco;
import com.hippo.yorozuya.collect.Multimap;
import com.hippo.yorozuya.collect.SparseILArray;
import java.util.List;
class Register<V> {
private final SparseILArray<ConacoTask<V>> mIdMap = new SparseILArray<>();
private final Multimap<String, ConacoTask<V>> mKeyMap = Multimap.create();
/**
* @return true for the key is already registered
*/
public synchronized boolean register(int id, ConacoTask<V> task) {
boolean repeatedKey = false;
String taskKey = task.getKey();
// For no-key task, there no need to check
if (taskKey != null) {
repeatedKey = mKeyMap.countElements(taskKey) != 0;
}
// Append task
mKeyMap.putElement(taskKey, task);
mIdMap.append(id, task);
return repeatedKey;
}
public synchronized ConacoTask<V> unregister(int id) {
ConacoTask<V> task = mIdMap.remove(id);
if (task != null) {
mKeyMap.removeElement(task.getKey(), task);
}
return task;
}
public synchronized boolean contain(int id) {
return mIdMap.indexOfKey(id) >= 0;
}
public synchronized ConacoTask<V> getByKey(String key) {
if (key == null) {
return null;
}
List<ConacoTask<V>> list = mKeyMap.get(key);
if (list != null && list.size() > 0) {
return list.get(0);
} else {
return null;
}
}
}
@@ -1,77 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* 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.hippo.conaco;
import androidx.annotation.NonNull;
public interface Unikery<V> {
int INVALID_ID = -1;
/**
* Get the conaco task id
*
* @return the conaco task id
*/
int getTaskId();
/**
* Store the conaco task id
*
* @param id the conaco task id
*/
void setTaskId(int id);
/**
* On miss in the source
*/
void onMiss(@Conaco.Source int source);
/**
* On start http request
*/
void onRequest();
/**
* On http request progress
*/
void onProgress(long singleReceivedSize, long receivedSize, long totalSize);
/**
* On start to wait because repeated key
*/
void onWait();
/**
* On get the value
*
* @return {@code true} for the {@code Unikery} accepts the value
* and the task ends. {@code false} for the {@code Unikery}
* reject the value and the task goes on or failed
*/
boolean onGetValue(@NonNull V value, @Conaco.Source int source);
/**
* On failed to get value
*/
void onFailure();
/**
* On user cancel the conaco task
*/
void onCancel();
}
@@ -1,61 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.conaco;
import androidx.annotation.NonNull;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.streampipe.InputStreamPipe;
import java.io.OutputStream;
class ValueCache<V> extends BeerBelly<V> {
private final ValueHelper<V> mHelper;
public ValueCache(BeerBelly.BeerBellyParams params, ValueHelper<V> helper) {
super(params);
mHelper = helper;
}
@Override
protected int sizeOf(String key, V value) {
return mHelper.sizeOf(key, value);
}
@Override
protected void memoryEntryAdded(V value) {
mHelper.onAddToMemoryCache(value);
}
@Override
protected void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue) {
if (oldValue != null) {
mHelper.onRemoveFromMemoryCache(key, oldValue);
}
}
@Override
protected V read(@NonNull InputStreamPipe isPipe) {
return mHelper.decode(isPipe);
}
@Override
protected boolean write(OutputStream os, V value) {
throw new UnsupportedOperationException("Not support write object");
}
}
@@ -1,55 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.conaco;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
public interface ValueHelper<V> {
/**
* Decode object for the {@code InputStreamPipe}
*
* @param isPipe the {@code InputStreamPipe}
* @return the decoded object
*/
@Nullable
V decode(@NonNull InputStreamPipe isPipe);
/**
* Get the size of the object
*/
int sizeOf(@NonNull String key, @NonNull V value);
/**
* Called when the object added to memory cache
*/
void onAddToMemoryCache(@NonNull V oldValue);
/**
* Called when the object removed from memory cache
*/
void onRemoveFromMemoryCache(@NonNull String key, @NonNull V oldValue);
/**
* Use cache memory or not. Sometimes large object
* should not be stored in memory cache.
*/
boolean useMemoryCache(@NonNull String key, V holder);
}
@@ -22,6 +22,7 @@ import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
@@ -29,17 +30,25 @@ import androidx.core.graphics.drawable.DrawableCompat;
public class DrawableWrapper extends Drawable implements Drawable.Callback {
private Drawable mDrawable;
private Rect mBounds;
public DrawableWrapper(Drawable drawable) {
this.setWrappedDrawable(drawable);
}
public void draw(@NonNull Canvas canvas) {
this.mDrawable.draw(canvas);
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
} else {
synchronized (mDrawable) {
mDrawable.setBounds(mBounds);
mDrawable.draw(canvas);
}
}
}
protected void onBoundsChange(Rect bounds) {
this.mDrawable.setBounds(bounds);
mBounds = bounds;
}
public int getChangingConfigurations() {
@@ -1,215 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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.hippo.drawable;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.Keep;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
/**
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
*/
public class DrawerArrowDrawable extends Drawable {
// The angle in degrees that the arrow head is inclined at.
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
private final Paint mPaint = new Paint();
private final float mBarThickness;
// The length of top and bottom bars when they merge into an arrow
private final float mTopBottomArrowSize;
// The length of middle bar
private final float mBarSize;
// The length of the middle bar when arrow is shaped
private final float mMiddleArrowSize;
// The space between bars when they are parallel
private final float mBarGap;
// Whether bars should spin or not during progress
private final boolean mSpin;
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
// wont look different
private final Path mPath = new Path();
// The reported intrinsic size of the drawable.
private final int mSize;
// the amount that overlaps w/ bar size when rotation is max
private final float mMaxCutForBarSize;
// Whether we should mirror animation when animation is reversed.
private boolean mVerticalMirror = false;
// The interpolated version of the original progress
private float mProgress;
/**
* @param context used to get the configuration for the drawable from
*/
public DrawerArrowDrawable(Context context, int color) {
Resources resources = context.getResources();
mPaint.setAntiAlias(true);
mPaint.setColor(color);
mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size);
// round this because having this floating may cause bad measurements
mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size));
// round this because having this floating may cause bad measurements
mTopBottomArrowSize = Math.round(resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size));
mBarThickness = resources.getDimension(R.dimen.dad_thickness);
// round this because having this floating may cause bad measurements
mBarGap = Math.round(resources.getDimension(R.dimen.dad_gap_between_bars));
mSpin = resources.getBoolean(R.bool.dad_spin_bars);
mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setStrokeCap(Paint.Cap.BUTT);
mPaint.setStrokeWidth(mBarThickness);
mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE));
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
protected void setVerticalMirror(boolean verticalMirror) {
mVerticalMirror = verticalMirror;
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
// Interpolated widths of arrow bars
final float arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress);
final float middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress);
// Interpolated size of middle bar
final float middleBarCut = Math.round(MathUtils.lerp(0, mMaxCutForBarSize, mProgress));
// The rotation of the top and bottom bars (that make the arrow head)
final float rotation = MathUtils.lerp(0, ARROW_HEAD_ANGLE, mProgress);
// The whole canvas rotates as the transition happens
final float canvasRotate = MathUtils.lerp(-180, 0, mProgress);
final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
mPath.rewind();
final float topBottomBarOffset = MathUtils.lerp(mBarGap + mBarThickness, -mMaxCutForBarSize,
mProgress);
final float arrowEdge = -middleBarSize / 2;
// draw middle bar
mPath.moveTo(arrowEdge + middleBarCut, 0);
mPath.rLineTo(middleBarSize - middleBarCut * 2, 0);
// bottom bar
mPath.moveTo(arrowEdge, topBottomBarOffset);
mPath.rLineTo(arrowWidth, arrowHeight);
// top bar
mPath.moveTo(arrowEdge, -topBottomBarOffset);
mPath.rLineTo(arrowWidth, -arrowHeight);
mPath.close();
canvas.save();
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
// the arrow pointing the other way for RTL.
canvas.translate(bounds.centerX(), bounds.centerY());
if (mSpin) {
canvas.rotate(canvasRotate * (mVerticalMirror ? -1 : 1));
}
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
public void setColor(@ColorInt int color) {
mPaint.setColor(color);
invalidateSelf();
}
@Override
public void setAlpha(int i) {
mPaint.setAlpha(i);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Keep
public float getProgress() {
return mProgress;
}
@Keep
public void setProgress(float progress) {
if (progress == 1f) {
setVerticalMirror(true);
} else if (progress == 0f) {
setVerticalMirror(false);
}
mProgress = progress;
invalidateSelf();
}
public void setMenu(long duration) {
setShape(false, duration);
}
public void setArrow(long duration) {
setShape(true, duration);
}
public void setShape(boolean arrow, long duration) {
if (!((!arrow && mProgress == 0f) || (arrow && mProgress == 1f))) {
float endProgress = arrow ? 1f : 0f;
if (duration <= 0) {
setProgress(endProgress);
} else {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
oa.setDuration(duration);
oa.setAutoCancel(true);
oa.start();
}
}
}
}
@@ -16,31 +16,25 @@
package com.hippo.drawable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.annotation.NonNull;
import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.Unikery;
import com.hippo.image.ImageBitmap;
import com.hippo.image.ImageDrawable;
import com.hippo.widget.ObservedTextView;
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
ObservedTextView.OnWindowAttachListener {
import coil.Coil;
import coil.request.Disposable;
import coil.request.ImageRequest;
public class UnikeryDrawable extends WrapDrawable implements ObservedTextView.OnWindowAttachListener {
private static final String TAG = UnikeryDrawable.class.getSimpleName();
private final ObservedTextView mTextView;
private final Conaco<ImageBitmap> mConaco;
private int mTaskId = Unikery.INVALID_ID;
private String mUrl;
private Disposable task;
public UnikeryDrawable(ObservedTextView textView, Conaco<ImageBitmap> conaco) {
public UnikeryDrawable(ObservedTextView textView) {
mTextView = textView;
mTextView.setOnWindowAttachListener(this);
mConaco = conaco;
}
@Override
@@ -50,22 +44,28 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
@Override
public void onDetachedFromWindow() {
mConaco.cancel(this);
if (task != null && !task.isDisposed())
task.dispose();
clearDrawable();
}
public void load(String url) {
if (url != null) {
mUrl = url;
mConaco.load(new ConacoTask.Builder<ImageBitmap>().setUnikery(this).setUrl(url).setKey(url));
var imageLoader = Coil.imageLoader(mTextView.getContext());
var request = new ImageRequest.Builder(mTextView.getContext()).data(url).memoryCacheKey(url).diskCacheKey(url).target(
drawable -> null,
drawable -> null,
drawable -> {
onGetValue(drawable);
return null;
}
).build();
task = imageLoader.enqueue(request);
}
}
private void clearDrawable() {
Drawable drawable = getDrawable();
if (drawable instanceof ImageDrawable) {
((ImageDrawable) drawable).recycle();
}
setDrawable(null);
}
@@ -89,63 +89,16 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
}
}
@Override
public int getTaskId() {
return mTaskId;
}
@Override
public void setTaskId(int id) {
mTaskId = id;
}
@Override
public void invalidateSelf() {
CharSequence cs = mTextView.getText();
mTextView.setText(cs);
}
@Override
public void onMiss(int source) {
}
@Override
public void onRequest() {
}
@Override
public void onProgress(long singleReceivedSize, long receivedSize, long totalSize) {
}
@Override
public void onWait() {
}
@Override
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
ImageDrawable drawable;
try {
drawable = new ImageDrawable(value);
} catch (Exception e) {
Log.d(TAG, "The ImageBitmap is recycled", e);
return false;
}
public void onGetValue(Drawable drawable) {
clearDrawable();
setDrawable(drawable);
drawable.start();
return true;
}
@Override
public void onFailure() {
// Empty
}
@Override
public void onCancel() {
// Empty
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
animatedImageDrawable.start();
}
}
@@ -64,8 +64,7 @@ public final class LayoutManagerUtils {
RecyclerView.LayoutManager layoutManager, Context context, int position,
int millisecondsPerInch) {
SimpleSmoothScroller smoothScroller;
if (layoutManager instanceof LinearLayoutManager) {
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (layoutManager instanceof final LinearLayoutManager linearLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -73,8 +72,7 @@ public final class LayoutManagerUtils {
}
};
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
} else if (layoutManager instanceof final StaggeredGridLayoutManager staggeredGridLayoutManager) {
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
@@ -1,596 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.ehviewer;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Debug;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.collection.LruCache;
import androidx.core.os.LocaleListCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.hippo.Native;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.conaco.Conaco;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhDns;
import com.hippo.ehviewer.client.EhEngine;
import com.hippo.ehviewer.client.EhRequestBuilder;
import com.hippo.ehviewer.client.EhSSLSocketFactory;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhX509TrustManager;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.parser.EventPaneParser;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.image.Image;
import com.hippo.image.ImageBitmap;
import com.hippo.network.StatusCodeException;
import com.hippo.scene.SceneApplication;
import com.hippo.unifile.UniFile;
import com.hippo.util.BitmapUtils;
import com.hippo.util.ClipboardUtil;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.OSUtils;
import com.hippo.yorozuya.SimpleHandler;
import java.io.File;
import java.io.IOException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class EhApplication extends SceneApplication {
public static final boolean BETA = false;
private static final String TAG = EhApplication.class.getSimpleName();
private static final String KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id";
private static final boolean DEBUG_CONACO = false;
private static final boolean DEBUG_PRINT_NATIVE_MEMORY = false;
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
private static final long DEBUG_PRINT_INTERVAL = 3000L;
public static boolean locked = false;
private static EhApplication instance;
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
private final HashMap<Integer, Object> mGlobalStuffMap = new HashMap<>();
private final List<Activity> mActivityList = new ArrayList<>();
private EhCookieStore mEhCookieStore;
private EhClient mEhClient;
private EhProxySelector mEhProxySelector;
private OkHttpClient mOkHttpClient;
private Cache mOkHttpCache;
private ImageBitmapHelper mImageBitmapHelper;
private Conaco<ImageBitmap> mConaco;
private LruCache<Long, GalleryDetail> mGalleryDetailCache;
private SimpleDiskCache mSpiderInfoCache;
private DownloadManager mDownloadManager;
private Hosts mHosts;
private FavouriteStatusRouter mFavouriteStatusRouter;
private boolean initialized = false;
public static EhApplication getInstance() {
return instance;
}
public static EhCookieStore getEhCookieStore(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhCookieStore == null) {
application.mEhCookieStore = new EhCookieStore(context);
}
return application.mEhCookieStore;
}
@NonNull
public static EhClient getEhClient(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhClient == null) {
application.mEhClient = new EhClient(application);
}
return application.mEhClient;
}
@NonNull
public static EhProxySelector getEhProxySelector(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhProxySelector == null) {
application.mEhProxySelector = new EhProxySelector();
}
return application.mEhProxySelector;
}
@NonNull
public static OkHttpClient getOkHttpClient(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.cookieJar(getEhCookieStore(application))
.cache(getOkHttpCache(application))
.dns(new EhDns(application))
.addNetworkInterceptor(chain -> {
try {
return chain.proceed(chain.request());
} catch (NullPointerException e) {
// crash on meizu devices due to old Android version
// https://github.com/square/okhttp/issues/3301#issuecomment-348415095
throw new IOException(e.getMessage());
}
})
.proxySelector(getEhProxySelector(application));
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
builder.sslSocketFactory(new EhSSLSocketFactory(), trustManager);
} catch (Exception e) {
e.printStackTrace();
builder.sslSocketFactory(new EhSSLSocketFactory(), new EhX509TrustManager());
}
application.mOkHttpClient = builder.build();
}
return application.mOkHttpClient;
}
@NonNull
public static Cache getOkHttpCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpCache == null) {
application.mOkHttpCache = new Cache(new File(application.getCacheDir(), "http_cache"), 50L * 1024L * 1024L);
}
return application.mOkHttpCache;
}
@NonNull
public static ImageBitmapHelper getImageBitmapHelper(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mImageBitmapHelper == null) {
application.mImageBitmapHelper = new ImageBitmapHelper();
}
return application.mImageBitmapHelper;
}
private static int getMemoryCacheMaxSize() {
return Math.min(20 * 1024 * 1024, (int) OSUtils.getAppMaxMemory());
}
@NonNull
public static Conaco<ImageBitmap> getConaco(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mConaco == null) {
Conaco.Builder<ImageBitmap> builder = new Conaco.Builder<>();
builder.hasMemoryCache = true;
builder.memoryCacheMaxSize = getMemoryCacheMaxSize();
builder.hasDiskCache = true;
builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
builder.diskCacheMaxSize = 320 * 1024 * 1024; // 320MB
builder.okHttpClient = getOkHttpClient(context);
builder.objectHelper = getImageBitmapHelper(context);
builder.debug = DEBUG_CONACO;
application.mConaco = builder.build();
}
return application.mConaco;
}
@NonNull
public static LruCache<Long, GalleryDetail> getGalleryDetailCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mGalleryDetailCache == null) {
// Max size 25, 3 min timeout
application.mGalleryDetailCache = new LruCache<>(25);
getFavouriteStatusRouter().addListener((gid, slot) -> {
GalleryDetail gd = application.mGalleryDetailCache.get(gid);
if (gd != null) {
gd.favoriteSlot = slot;
}
});
}
return application.mGalleryDetailCache;
}
@NonNull
public static SimpleDiskCache getSpiderInfoCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (null == application.mSpiderInfoCache) {
application.mSpiderInfoCache = new SimpleDiskCache(
new File(context.getCacheDir(), "spider_info"), 20 * 1024 * 1024); // 20M
}
return application.mSpiderInfoCache;
}
@NonNull
public static DownloadManager getDownloadManager() {
return getDownloadManager(instance);
}
@NonNull
public static DownloadManager getDownloadManager(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mDownloadManager == null) {
application.mDownloadManager = new DownloadManager(application);
}
return application.mDownloadManager;
}
@NonNull
public static Hosts getHosts(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mHosts == null) {
application.mHosts = new Hosts(application, "hosts.db");
}
return application.mHosts;
}
@NonNull
public static FavouriteStatusRouter getFavouriteStatusRouter() {
return getFavouriteStatusRouter(getInstance());
}
@NonNull
public static FavouriteStatusRouter getFavouriteStatusRouter(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mFavouriteStatusRouter == null) {
application.mFavouriteStatusRouter = new FavouriteStatusRouter();
}
return application.mFavouriteStatusRouter;
}
@SuppressLint("StaticFieldLeak")
@Override
public void onCreate() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
instance = this;
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
try {
// Always save crash file if onCreate() is not done
if (!initialized || Settings.getSaveCrashLog()) {
Crash.saveCrashLog(instance, e);
}
} catch (Throwable ignored) {
}
if (handler != null) {
handler.uncaughtException(t, e);
}
});
super.onCreate();
Native.initialize();
ClipboardUtil.initialize(this);
GetText.initialize(this);
StatusCodeException.initialize(this);
Settings.initialize(this);
ReadableTime.initialize(this);
AppConfig.initialize(this);
SpiderDen.initialize(this);
EhDB.initialize(this);
EhEngine.initialize();
BitmapUtils.initialize(this);
if (EhDB.needMerge()) {
EhDB.mergeOldDB(this);
}
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
// Do io tasks in new thread
//noinspection deprecation
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
// Check no media file
try {
UniFile downloadLocation = Settings.getDownloadLocation();
if (Settings.getMediaScan()) {
CommonOperations.removeNoMediaFile(downloadLocation);
} else {
CommonOperations.ensureNoMediaFile(downloadLocation);
}
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
// Clear temp files
try {
clearTempDir();
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
return null;
}
}.executeOnExecutor(IoThreadPoolExecutor.getInstance());
// Check app update
update();
// Update version code
try {
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
Settings.putVersionCode(pi.versionCode);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0));
if (DEBUG_PRINT_NATIVE_MEMORY || DEBUG_PRINT_IMAGE_COUNT) {
debugPrint();
}
initialized = true;
theDawnOfNewDay();
}
private void theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return;
}
EhCookieStore store = getEhCookieStore(this);
HttpUrl eh = HttpUrl.get(EhUrl.HOST_E);
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(eh, EhCookieStore.KEY_IPD_PASS_HASH)) {
IoThreadPoolExecutor.getInstance().execute(() -> {
String referer = EhUrl.REFERER_E;
Request request = new EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build();
Call call = getOkHttpClient(this).newCall(request);
try {
Response response = call.execute();
ResponseBody responseBody = response.body();
if (responseBody == null) {
return;
}
String body = responseBody.string();
String html = EventPaneParser.parse(body);
if (html != null) {
showEventPane(html);
}
} catch (Throwable e) {
e.printStackTrace();
}
});
}
}
public void showEventPane(String html) {
Activity activity = getTopActivity();
if (activity != null) {
activity.runOnUiThread(() -> {
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
.setMessage(Html.fromHtml(html))
.setPositiveButton(android.R.string.ok, null)
.create();
dialog.setOnShowListener(d -> {
final View messageView = dialog.findViewById(android.R.id.message);
if (messageView instanceof TextView) {
((TextView) messageView).setMovementMethod(LinkMovementMethod.getInstance());
}
});
try {
dialog.show();
} catch (Throwable t) {
// ignore
}
});
}
}
private void clearTempDir() {
File dir = AppConfig.getTempDir();
if (null != dir) {
FileUtils.deleteContent(dir);
}
dir = AppConfig.getExternalTempDir();
if (null != dir) {
FileUtils.deleteContent(dir);
}
}
private void update() {
int version = Settings.getVersionCode();
if (version < 52) {
Settings.putGuideGallery(true);
}
}
public void clearMemoryCache() {
if (null != mConaco) {
mConaco.getBeerBelly().clearMemory();
}
if (null != mGalleryDetailCache) {
mGalleryDetailCache.evictAll();
}
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
clearMemoryCache();
}
}
private void debugPrint() {
new Runnable() {
@Override
public void run() {
if (DEBUG_PRINT_NATIVE_MEMORY) {
Log.i(TAG, "Native memory: " + FileUtils.humanReadableByteCount(
Debug.getNativeHeapAllocatedSize(), false));
}
if (DEBUG_PRINT_IMAGE_COUNT) {
Log.i(TAG, "Image count: " + Image.getImageCount());
}
SimpleHandler.getInstance().postDelayed(this, DEBUG_PRINT_INTERVAL);
}
}.run();
}
public int putGlobalStuff(@NonNull Object o) {
int id = mIdGenerator.nextId();
mGlobalStuffMap.put(id, o);
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId());
return id;
}
public boolean containGlobalStuff(int id) {
return mGlobalStuffMap.containsKey(id);
}
public Object getGlobalStuff(int id) {
return mGlobalStuffMap.get(id);
}
public Object removeGlobalStuff(int id) {
return mGlobalStuffMap.remove(id);
}
public void removeGlobalStuff(Object o) {
mGlobalStuffMap.values().removeAll(Collections.singleton(o));
}
public void registerActivity(Activity activity) {
mActivityList.add(activity);
}
public void unregisterActivity(Activity activity) {
mActivityList.remove(activity);
}
@Nullable
public Activity getTopActivity() {
if (!mActivityList.isEmpty()) {
return mActivityList.get(mActivityList.size() - 1);
} else {
return null;
}
}
// Avoid crash on some "energy saving" devices
@Override
public ComponentName startService(Intent service) {
try {
return super.startService(service);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return null;
}
}
// Avoid crash on some "energy saving" devices
@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
try {
return super.bindService(service, conn, flags);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return false;
}
}
// Avoid crash on some "energy saving" devices
@Override
public void unbindService(ServiceConnection conn) {
try {
super.unbindService(conn);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
}
// Avoid crash on some "energy saving" devices
@Override
public ComponentName startForegroundService(Intent service) {
try {
return super.startForegroundService(service);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return null;
}
}
static class EhlifecycleObserver implements DefaultLifecycleObserver {
@Override
public void onPause(@NonNull LifecycleOwner owner) {
locked = true;
}
}
}
@@ -0,0 +1,329 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.ehviewer
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentCallbacks2
import android.os.Build
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.Coil
import coil.ImageLoader
import com.hippo.Native
import com.hippo.app.BaseDialogBuilder
import com.hippo.beerbelly.SimpleDiskCache
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhX509TrustManager
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.scene.SceneApplication
import com.hippo.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.IoThreadPoolExecutor
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import java.security.KeyStore
import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class EhApplication : SceneApplication() {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap<Int, Any>()
private val mActivityList = ArrayList<Activity>()
private var initialized = false
val topActivity: EhActivity?
get() = if (mActivityList.isNotEmpty()) {
mActivityList[mActivityList.size - 1] as EhActivity
} else {
null
}
fun recreateAllActivity() {
mActivityList.forEach { it.recreate() }
}
@SuppressLint("StaticFieldLeak")
override fun onCreate() {
ProcessLifecycleOwner.get().lifecycle.addObserver(EhlifecycleObserver())
application = this
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(application, e)
}
} catch (ignored: Throwable) {
}
handler?.uncaughtException(t, e)
}
super.onCreate()
Native.initialize()
GetText.initialize(this)
Settings.initialize(this)
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
EhDB.initialize(this)
EhEngine.initialize()
BitmapUtils.initialize(this)
Coil.setImageLoader(ImageLoader.Builder(this).okHttpClient(okHttpClient).build())
// Locales can be managed by system automatically above Snow Cone v2
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()))
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
IoThreadPoolExecutor.getInstance().execute {
try {
val downloadLocation = Settings.getDownloadLocation()
if (Settings.getMediaScan()) {
CommonOperations.removeNoMediaFile(downloadLocation)
} else {
CommonOperations.ensureNoMediaFile(downloadLocation)
}
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
}
// Clear temp files
try {
clearTempDir()
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
initialized = true
theDawnOfNewDay()
}
private fun theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return
}
val store = ehCookieStore
val eh = EhUrl.HOST_E.toHttpUrl()
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(
eh,
EhCookieStore.KEY_IPD_PASS_HASH
)
) {
IoThreadPoolExecutor.getInstance().execute {
val referer = EhUrl.REFERER_E
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
val call = okHttpClient.newCall(request)
try {
val response = call.execute()
val responseBody = response.body
val body = responseBody.string()
val html = EventPaneParser.parse(body)
if (html != null) {
showEventPane(html)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
}
fun showEventPane(html: String) {
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
return;
}
val activity = topActivity
activity?.runOnUiThread {
val dialog = BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.setOnShowListener {
val messageView = dialog.findViewById<View>(android.R.id.message)
if (messageView is TextView) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}
try {
dialog.show()
} catch (t: Throwable) {
// ignore
}
}
}
private fun clearTempDir() {
var dir = AppConfig.getTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
dir = AppConfig.getExternalTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
galleryDetailCache.evictAll()
}
}
fun putGlobalStuff(o: Any): Int {
val id = mIdGenerator.nextId()
mGlobalStuffMap[id] = o
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
return id
}
fun containGlobalStuff(id: Int): Boolean {
return mGlobalStuffMap.containsKey(id)
}
fun getGlobalStuff(id: Int): Any? {
return mGlobalStuffMap[id]
}
fun removeGlobalStuff(id: Int): Any? {
return mGlobalStuffMap.remove(id)
}
fun removeGlobalStuff(o: Any) {
mGlobalStuffMap.values.removeAll(setOf(o))
}
fun registerActivity(activity: Activity) {
mActivityList.add(activity)
}
fun unregisterActivity(activity: Activity) {
mActivityList.remove(activity)
}
internal class EhlifecycleObserver : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
if (!locked)
locked_last_leave_time = System.currentTimeMillis() / 1000
locked = true
}
}
companion object {
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
var locked = true
var locked_last_leave_time: Long = 0
@JvmStatic
lateinit var application: EhApplication
private set
@JvmStatic
val ehCookieStore by lazy { EhCookieStore(application) }
@JvmStatic
val ehClient by lazy { EhClient(application) }
@JvmStatic
val ehProxySelector by lazy { EhProxySelector() }
@JvmStatic
val okHttpClient by lazy {
val builder = OkHttpClient.Builder()
.cookieJar(ehCookieStore)
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
.dns(EhDns())
.proxySelector(ehProxySelector)
if (Settings.getDF()) {
var trustManager: X509TrustManager
try {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
"Unexpected default trust managers:" + Arrays.toString(
trustManagers
)
}
trustManager = trustManagers[0] as X509TrustManager
} catch (e: Exception) {
e.printStackTrace()
trustManager = EhX509TrustManager()
}
builder.sslSocketFactory(EhSSLSocketFactory(), trustManager)
}
builder.build()
}
@JvmStatic
val galleryDetailCache by lazy {
LruCache<Long, GalleryDetail>(25).also {
favouriteStatusRouter.addListener { gid, slot ->
it[gid]?.favoriteSlot = slot
}
}
}
@JvmStatic
val spiderInfoCache by lazy {
SimpleDiskCache(
File(application.cacheDir, "spider_info"),
20 * 1024 * 1024
)
}
@JvmStatic
val downloadManager by lazy { DownloadManager(application) }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@JvmStatic
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
}
}
+152 -410
View File
@@ -17,27 +17,24 @@
package com.hippo.ehviewer;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.room.Room;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.dao.DaoMaster;
import com.hippo.ehviewer.dao.DaoSession;
import com.hippo.ehviewer.dao.BasicDao;
import com.hippo.ehviewer.dao.DownloadDirname;
import com.hippo.ehviewer.dao.DownloadDirnameDao;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.DownloadLabelDao;
import com.hippo.ehviewer.dao.DownloadsDao;
import com.hippo.ehviewer.dao.EhDatabase;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.dao.FilterDao;
import com.hippo.ehviewer.dao.HistoryDao;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
@@ -46,14 +43,8 @@ import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.dao.QuickSearchDao;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.SqlUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.collect.SparseJLArray;
import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.query.CloseableListIterator;
import org.greenrobot.greendao.query.LazyList;
import java.io.File;
import java.io.FileInputStream;
@@ -65,15 +56,9 @@ import java.util.ArrayList;
import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static final String TAG = EhDB.class.getSimpleName();
private static final int MAX_HISTORY_COUNT = 200;
private static DaoSession sDaoSession;
private static boolean sHasOldDB;
private static boolean sNewDB;
private static EhDatabase db;
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
@@ -115,219 +100,12 @@ public class EhDB {
}
public static void initialize(Context context) {
sHasOldDB = context.getDatabasePath("data").exists();
DBOpenHelper helper = new DBOpenHelper(
context.getApplicationContext(), "eh.db", null);
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
sDaoSession = daoMaster.newSession();
}
public static boolean needMerge() {
return sNewDB && sHasOldDB;
}
public static void mergeOldDB(Context context) {
sNewDB = false;
OldDBHelper oldDBHelper = new OldDBHelper(context);
SQLiteDatabase oldDB;
try {
oldDB = oldDBHelper.getReadableDatabase();
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return;
}
// Get GalleryInfo list
SparseJLArray<GalleryInfo> map = new SparseJLArray<>();
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_GALLERY, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
GalleryInfo gi = new GalleryInfo();
gi.gid = cursor.getInt(0);
gi.token = cursor.getString(1);
gi.title = cursor.getString(2);
gi.posted = cursor.getString(3);
gi.category = cursor.getInt(4);
gi.thumb = cursor.getString(5);
gi.uploader = cursor.getString(6);
try {
// In 0.6.x version, NaN is stored
gi.rating = cursor.getFloat(7);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gi.rating = -1.0f;
}
map.put(gi.gid, gi);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge local favorites
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_LOCAL_FAVOURITE, null);
if (cursor != null) {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
if (cursor.moveToFirst()) {
long i = 0L;
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
LocalFavoriteInfo info = new LocalFavoriteInfo(gi);
info.setTime(i);
dao.insert(info);
cursor.moveToNext();
i++;
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge quick search
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_TAG, null);
if (cursor != null) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
QuickSearch quickSearch = new QuickSearch();
int mode = cursor.getInt(2);
String search = cursor.getString(4);
String tag = cursor.getString(7);
if (mode == ListUrlBuilder.MODE_UPLOADER && search != null &&
search.startsWith("uploader:")) {
search = search.substring("uploader:".length());
}
quickSearch.setTime(cursor.getInt(0));
quickSearch.setName(cursor.getString(1));
quickSearch.setMode(mode);
quickSearch.setCategory(cursor.getInt(3));
quickSearch.setKeyword(mode == ListUrlBuilder.MODE_TAG ? tag : search);
quickSearch.setAdvanceSearch(cursor.getInt(5));
quickSearch.setMinRating(cursor.getInt(6));
dao.insert(quickSearch);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge download info
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_DOWNLOAD, null);
if (cursor != null) {
DownloadsDao dao = sDaoSession.getDownloadsDao();
if (cursor.moveToFirst()) {
long i = 0L;
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
DownloadInfo info = new DownloadInfo(gi);
int state = cursor.getInt(2);
int legacy = cursor.getInt(3);
if (state == DownloadInfo.STATE_FINISH && legacy > 0) {
state = DownloadInfo.STATE_FAILED;
}
info.setState(state);
info.setLegacy(legacy);
if (cursor.getColumnCount() == 5) {
info.setTime(cursor.getLong(4));
} else {
info.setTime(i);
}
dao.insert(info);
cursor.moveToNext();
i++;
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
try {
// Merge history info
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_HISTORY, null);
if (cursor != null) {
HistoryDao dao = sDaoSession.getHistoryDao();
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
HistoryInfo info = new HistoryInfo(gi);
info.setMode(cursor.getInt(1));
info.setTime(cursor.getLong(2));
dao.insert(info);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
try {
oldDBHelper.close();
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
db = Room.databaseBuilder(context, EhDatabase.class, "eh.db").allowMainThreadQueries().build();
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
DownloadsDao dao = sDaoSession.getDownloadsDao();
List<DownloadInfo> list = dao.queryBuilder().orderDesc(DownloadsDao.Properties.Time).list();
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list();
// Fix state
for (DownloadInfo info : list) {
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
@@ -339,7 +117,7 @@ public class EhDB {
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = sDaoSession.getDownloadsDao();
DownloadsDao dao = db.downloadsDao();
if (null != dao.load(downloadInfo.gid)) {
// Update
dao.update(downloadInfo);
@@ -349,13 +127,13 @@ public class EhDB {
}
}
public static synchronized void removeDownloadInfo(long gid) {
sDaoSession.getDownloadsDao().deleteByKey(gid);
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
db.downloadsDao().delete(downloadInfo);
}
@Nullable
public static synchronized String getDownloadDirname(long gid) {
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) {
return raw.getDirname();
@@ -368,7 +146,7 @@ public class EhDB {
* Insert or update
*/
public static synchronized void putDownloadDirname(long gid, String dirname) {
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) { // Update
raw.setDirname(dirname);
@@ -382,23 +160,23 @@ public class EhDB {
}
public static synchronized void removeDownloadDirname(long gid) {
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteByKey(gid);
}
public static synchronized void clearDownloadDirname() {
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteAll();
}
@NonNull
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
return dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time).list();
DownloadLabelDao dao = db.downloadLabelDao();
return dao.list();
}
public static synchronized DownloadLabel addDownloadLabel(String label) {
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabel raw = new DownloadLabel();
raw.setLabel(label);
raw.setTime(System.currentTimeMillis());
@@ -409,13 +187,13 @@ public class EhDB {
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
// Reset id
raw.setId(null);
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
DownloadLabelDao dao = db.downloadLabelDao();
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
DownloadLabelDao dao = db.downloadLabelDao();
dao.update(raw);
}
@@ -428,9 +206,8 @@ public class EhDB {
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
List<DownloadLabel> list = dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time)
.offset(offset).limit(limit).list();
DownloadLabelDao dao = db.downloadLabelDao();
List<DownloadLabel> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
@@ -441,50 +218,44 @@ public class EhDB {
}
list.get(start).setTime(toTime);
dao.updateInTx(list);
dao.update(list);
}
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
DownloadLabelDao dao = db.downloadLabelDao();
dao.delete(raw);
}
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time).list();
List<GalleryInfo> result = new ArrayList<>();
result.addAll(list);
return result;
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
}
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
query = SqlUtils.sqlEscapeString("%" + query + "%");
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time)
.where(LocalFavoritesDao.Properties.Title.like(query)).list();
List<GalleryInfo> result = new ArrayList<>();
result.addAll(list);
return result;
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
}
public static synchronized void removeLocalFavorites(long gid) {
sDaoSession.getLocalFavoritesDao().deleteByKey(gid);
db.localFavoritesDao().deleteByKey(gid);
}
public static synchronized void removeLocalFavorites(long[] gidArray) {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
LocalFavoritesDao dao = db.localFavoritesDao();
for (long gid : gidArray) {
dao.deleteByKey(gid);
}
}
public static synchronized boolean containLocalFavorites(long gid) {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
LocalFavoritesDao dao = db.localFavoritesDao();
return null != dao.load(gid);
}
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.gid)) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
@@ -504,31 +275,31 @@ public class EhDB {
}
public static synchronized List<QuickSearch> getAllQuickSearch() {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
return dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time).list();
QuickSearchDao dao = db.quickSearchDao();
return dao.list();
}
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
QuickSearchDao dao = db.quickSearchDao();
quickSearch.id = null;
quickSearch.time = System.currentTimeMillis();
quickSearch.id = dao.insert(quickSearch);
}
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
QuickSearchDao dao = db.quickSearchDao();
for (QuickSearch quickSearch : quickSearchList) {
dao.insert(quickSearch);
}
}
public static synchronized void updateQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
QuickSearchDao dao = db.quickSearchDao();
dao.update(quickSearch);
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
QuickSearchDao dao = db.quickSearchDao();
dao.delete(quickSearch);
}
@@ -541,9 +312,8 @@ public class EhDB {
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
List<QuickSearch> list = dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time)
.offset(offset).limit(limit).list();
QuickSearchDao dao = db.quickSearchDao();
List<QuickSearch> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
@@ -554,15 +324,19 @@ public class EhDB {
}
list.get(start).setTime(toTime);
dao.updateInTx(list);
dao.update(list);
}
public static synchronized LazyList<HistoryInfo> getHistoryLazyList() {
return sDaoSession.getHistoryDao().queryBuilder().orderDesc(HistoryDao.Properties.Time).listLazy();
public static synchronized List<HistoryInfo> getHistoryList() {
return db.historyDao().list();
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryDao dao = db.historyDao();
HistoryInfo info = dao.load(galleryInfo.gid);
if (null != info) {
// Update time
@@ -573,49 +347,42 @@ public class EhDB {
info = new HistoryInfo(galleryInfo);
info.time = System.currentTimeMillis();
dao.insert(info);
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
.limit(-1).offset(MAX_HISTORY_COUNT).list();
dao.deleteInTx(list);
}
}
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryDao dao = db.historyDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info);
}
}
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
.limit(-1).offset(MAX_HISTORY_COUNT).list();
dao.deleteInTx(list);
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryDao dao = db.historyDao();
dao.delete(info);
}
public static synchronized void clearHistoryInfo() {
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryDao dao = db.historyDao();
dao.deleteAll();
}
public static synchronized List<Filter> getAllFilter() {
return sDaoSession.getFilterDao().queryBuilder().list();
return db.filterDao().list();
}
public static synchronized boolean addFilter(Filter filter) {
Filter existFilter;
try {
existFilter = sDaoSession.getFilterDao().queryBuilder().where(FilterDao.Properties.Text.eq(filter.text), FilterDao.Properties.Mode.eq(filter.mode)).unique();
existFilter = db.filterDao().load(filter.text, filter.mode);
} catch (Exception e) {
existFilter = null;
}
if (existFilter == null) {
filter.setId(null);
filter.setId(sDaoSession.getFilterDao().insert(filter));
filter.setId(db.filterDao().insert(filter));
return true;
} else {
return false;
@@ -623,23 +390,18 @@ public class EhDB {
}
public static synchronized void deleteFilter(Filter filter) {
sDaoSession.getFilterDao().delete(filter);
db.filterDao().delete(filter);
}
public static synchronized void triggerFilter(Filter filter) {
filter.setEnable(!filter.enable);
sDaoSession.getFilterDao().update(filter);
db.filterDao().update(filter);
}
private static <T> boolean copyDao(AbstractDao<T, ?> from, AbstractDao<T, ?> to) {
try (CloseableListIterator<T> iterator = from.queryBuilder().listIterator()) {
while (iterator.hasNext()) {
to.insert(iterator.next());
}
} catch (IOException e) {
return false;
}
return true;
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
List<T> list = from.list();
for (T item : list)
to.insert(item);
}
public static synchronized boolean exportDB(Context context, Uri uri) {
@@ -647,30 +409,21 @@ public class EhDB {
// Delete old export db
context.deleteDatabase(ehExportName);
DBOpenHelper helper = new DBOpenHelper(context.getApplicationContext(), ehExportName, null);
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
try {
// Copy data to a export db
try (SQLiteDatabase db = helper.getWritableDatabase()) {
DaoMaster daoMaster = new DaoMaster(db);
DaoSession exportSession = daoMaster.newSession();
if (!copyDao(sDaoSession.getDownloadsDao(), exportSession.getDownloadsDao()))
return false;
if (!copyDao(sDaoSession.getDownloadLabelDao(), exportSession.getDownloadLabelDao()))
return false;
if (!copyDao(sDaoSession.getDownloadDirnameDao(), exportSession.getDownloadDirnameDao()))
return false;
if (!copyDao(sDaoSession.getHistoryDao(), exportSession.getHistoryDao()))
return false;
if (!copyDao(sDaoSession.getQuickSearchDao(), exportSession.getQuickSearchDao()))
return false;
if (!copyDao(sDaoSession.getLocalFavoritesDao(), exportSession.getLocalFavoritesDao()))
return false;
if (!copyDao(sDaoSession.getBookmarksDao(), exportSession.getBookmarksDao()))
return false;
if (!copyDao(sDaoSession.getFilterDao(), exportSession.getFilterDao()))
return false;
copyDao(db.downloadsDao(), newDb.downloadsDao());
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
copyDao(db.historyDao(), newDb.historyDao());
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
copyDao(db.filterDao(), newDb.filterDao());
// Close export db so we can copy it
if (newDb.isOpen()) {
newDb.close();
}
// Copy export db to data dir
@@ -691,6 +444,7 @@ public class EhDB {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
// Delete failed file
return false;
} finally {
@@ -704,6 +458,8 @@ public class EhDB {
public static synchronized String importDB(Context context, Uri uri) {
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
// Copy file to cache dir
File file = File.createTempFile("importDatabase", "");
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buff = new byte[1024];
@@ -718,77 +474,105 @@ public class EhDB {
inputStream.close();
outputStream.close();
SQLiteDatabase db = SQLiteDatabase.openDatabase(
// Check database version
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
int newVersion = DaoMaster.SCHEMA_VERSION;
int oldVersion = db.getVersion();
int newVersion = CUR_DB_VER;
int oldVersion = oldDB.getVersion();
if (oldVersion < newVersion) {
upgradeDB(db, oldVersion);
db.setVersion(newVersion);
upgradeDB(oldDB, oldVersion);
oldDB.setVersion(newVersion);
} else if (oldVersion > newVersion) {
return context.getString(R.string.cant_read_the_file);
return context.getString(R.string.db_version_too_high);
}
DaoMaster daoMaster = new DaoMaster(db);
DaoSession session = daoMaster.newSession();
// Downloads
DownloadManager manager = EhApplication.getDownloadManager(context);
List<DownloadInfo> downloadInfoList = session.getDownloadsDao().queryBuilder().list();
manager.addDownload(downloadInfoList, false);
// Crete temp room database from cache file
String tmpDBName = "tmp.db";
context.deleteDatabase(tmpDBName);
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
.createFromFile(file).allowMainThreadQueries().build();
// Download label
List<DownloadLabel> downloadLabelList = session.getDownloadLabelDao().queryBuilder().list();
manager.addDownloadLabel(downloadLabelList);
DownloadManager manager = EhApplication.getDownloadManager();
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
} catch (Exception ignored) {
}
// Downloads
try {
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
manager.addDownload(downloadInfoList, false);
} catch (Exception ignored) {
}
// Download dirname
List<DownloadDirname> downloadDirnameList = session.getDownloadDirnameDao().queryBuilder().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
try {
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {
}
// History
List<HistoryInfo> historyInfoList = session.getHistoryDao().queryBuilder().list();
putHistoryInfo(historyInfoList);
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {
}
// QuickSearch
List<QuickSearch> quickSearchList = session.getQuickSearchDao().queryBuilder().list();
List<QuickSearch> currentQuickSearchList = sDaoSession.getQuickSearchDao().queryBuilder().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.name;
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.name, name)) {
// The same name
name = null;
break;
try {
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.name;
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.name, name)) {
// The same name
name = null;
break;
}
}
if (null == name) {
continue;
}
importList.add(quickSearch);
}
if (null == name) {
continue;
}
importList.add(quickSearch);
importQuickSearch(importList);
} catch (Exception ignored) {
}
importQuickSearch(importList);
// LocalFavorites
List<LocalFavoriteInfo> localFavoriteInfoList = session.getLocalFavoritesDao().queryBuilder().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
try {
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {
}
// Bookmarks
// TODO
// Filter
List<Filter> filterList = session.getFilterDao().queryBuilder().list();
List<Filter> currentFilterList = sDaoSession.getFilterDao().queryBuilder().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
try {
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
}
}
} catch (Exception ignored) {
}
// Delete temp database
if (oldRoomDatabase.isOpen()) {
oldRoomDatabase.close();
}
context.deleteDatabase(tmpDBName);
return null;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -796,46 +580,4 @@ public class EhDB {
return context.getString(R.string.cant_read_the_file);
}
}
private static class DBOpenHelper extends DaoMaster.OpenHelper {
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onCreate(SQLiteDatabase db) {
super.onCreate(db);
sNewDB = true;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
upgradeDB(db, oldVersion);
}
}
private static class OldDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "data";
private static final int VERSION = 4;
private static final String TABLE_GALLERY = "gallery";
private static final String TABLE_LOCAL_FAVOURITE = "local_favourite";
private static final String TABLE_TAG = "tag";
private static final String TABLE_DOWNLOAD = "download";
private static final String TABLE_HISTORY = "history";
public OldDBHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
@@ -1,75 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.ehviewer;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.conaco.ValueHelper;
import com.hippo.image.ImageBitmap;
import com.hippo.streampipe.InputStreamPipe;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
private static final int MAX_CACHE_SIZE = 512 * 512;
@Nullable
@Override
public ImageBitmap decode(@NonNull InputStreamPipe isPipe) {
try {
isPipe.obtain();
FileInputStream is = (FileInputStream) isPipe.open();
return ImageBitmap.decode(is.getFD());
} catch (OutOfMemoryError | IOException e) {
return null;
} finally {
isPipe.close();
isPipe.release();
}
}
@Override
public int sizeOf(@NonNull String key, @NonNull ImageBitmap value) {
return value.getWidth() * value.getHeight() * 4 /* value.getByteCount() TODO Update Image */;
}
@Override
public void onAddToMemoryCache(@NonNull ImageBitmap oldValue) {
oldValue.obtain();
}
@Override
public void onRemoveFromMemoryCache(@NonNull String key, @NonNull ImageBitmap oldValue) {
oldValue.release();
}
@Override
public boolean useMemoryCache(@NonNull String key, ImageBitmap value) {
if (value != null) {
return value.getWidth() * value.getHeight() <= MAX_CACHE_SIZE
/* value.getByteCount() <= MAX_CACHE_BYTE_COUNT TODO Update Image */;
} else {
return true;
}
}
}
@@ -50,6 +50,8 @@ public class Settings {
********************/
public static final String KEY_THEME = "theme";
public static final String KEY_ACCOUNT = "account";
public static final String KEY_BLACK_DARK_THEME = "black_dark_theme";
public static final int THEME_LIGHT = 1;
public static final int THEME_SYSTEM = -1;
@@ -58,13 +60,15 @@ public class Settings {
public static final String KEY_GALLERY_SITE = "gallery_site";
public static final String KEY_LIST_MODE = "list_mode";
public static final String KEY_DETAIL_SIZE = "detail_size";
public static final String KEY_THUMB_SIZE = "thumb_size";
public static final String KEY_THUMB_SIZE = "thumb_size_";
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY;
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
private static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
/********************
****** Privacy and Security
********************/
@@ -76,6 +80,7 @@ public class Settings {
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
public static final String KEY_MEDIA_SCAN = "media_scan";
@@ -88,6 +93,7 @@ public class Settings {
********************/
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
public static final String KEY_SECURITY = "require_unlock";
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
public static final String DEFAULT_SECURITY = "";
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
@@ -95,11 +101,17 @@ public class Settings {
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_APP_LANGUAGE = "app_language";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String KEY_PREVIEW_NUM = "preview_num";
private static final int DEFAULT_PREVIEW_NUM = 60;
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_VERSION_CODE = "version_code";
private static final int DEFAULT_VERSION_CODE = 0;
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
private static final String KEY_AVATAR = "avatar";
private static final String DEFAULT_AVATAR = null;
private static final String KEY_SHOW_WARNING = "show_warning";
@@ -119,10 +131,8 @@ public class Settings {
private static final int DEFAULT_LAUNCH_PAGE = 0;
private static final int DEFAULT_LIST_MODE = 0;
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 1;
private static final int DEFAULT_THUMB_SIZE = 120;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_FIX_THUMB_URL = "fix_thumb_url";
private static final boolean DEFAULT_FIX_THUMB_URL = false;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
private static final String KEY_SHOW_GALLERY_PAGES = "show_gallery_pages";
@@ -238,7 +248,6 @@ public class Settings {
****** Update
********************/
private static final String KEY_BETA_UPDATE_CHANNEL = "beta_update_channel";
private static final boolean DEFAULT_BETA_UPDATE_CHANNEL = EhApplication.BETA;
private static final String KEY_SKIP_UPDATE_VERSION = "skip_update_version";
private static final int DEFAULT_SKIP_UPDATE_VERSION = 0;
private static final boolean DEFAULT_SAVE_PARSE_ERROR_BODY = true;
@@ -272,11 +281,9 @@ public class Settings {
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
public static boolean LIST_THUMB_SIZE_INITED = false;
private static int LIST_THUMB_SIZE = 40;
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static EhConfig sEhConfig;
@@ -418,14 +425,7 @@ public class Settings {
sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
}
public static int getVersionCode() {
return getInt(KEY_VERSION_CODE, DEFAULT_VERSION_CODE);
}
public static void putVersionCode(int value) {
putInt(KEY_VERSION_CODE, value);
}
@Nullable
public static String getDisplayName() {
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
}
@@ -533,34 +533,30 @@ public class Settings {
}
public static int getThumbSize() {
return getIntFromStr(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE);
return dip2px(getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE));
}
@DimenRes
public static int getThumbSizeResId() {
switch (getThumbSize()) {
case 0:
return R.dimen.gallery_grid_column_width_large;
default:
case 1:
return R.dimen.gallery_grid_column_width_middle;
case 2:
return R.dimen.gallery_grid_column_width_small;
}
public static int dip2px(int dpValue) {
final float scale = sContext.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int getThumbResolution() {
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
}
public static boolean getFixThumbUrl() {
return getBoolean(KEY_FIX_THUMB_URL, DEFAULT_FIX_THUMB_URL);
public static boolean getShowComments() {
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
}
public static boolean getRequestNews() {
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
}
public static boolean getHideHvEvents() {
return getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS);
}
public static boolean getShowJpnTitle() {
return getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE);
}
@@ -577,6 +573,10 @@ public class Settings {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
}
public static int getPreviewNum() {
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
}
public static int getDefaultCategories() {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
@@ -968,11 +968,11 @@ public class Settings {
boolean writeFile = false;
String userID = getString(KEY_USER_ID, null);
File file = AppConfig.getFileInExternalAppDir(FILENAME_USER_ID);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeXml = true;
// Get use ID from out sd card file
userID = FileUtils.read(file);
if (null == userID || !isValidUserID(userID)) {
if (!isValidUserID(userID)) {
writeFile = true;
userID = generateUserID();
}
@@ -1021,10 +1021,6 @@ public class Settings {
return true;
}
public static boolean getBetaUpdateChannel() {
return getBoolean(KEY_BETA_UPDATE_CHANNEL, DEFAULT_BETA_UPDATE_CHANNEL);
}
public static void putBetaUpdateChannel(boolean value) {
putBoolean(KEY_BETA_UPDATE_CHANNEL, value);
}
@@ -1057,6 +1053,10 @@ public class Settings {
putBoolean(KEY_SECURITY, value);
}
public static int getSecurityDelay() {
return getInt(KEY_SECURITY_DELAY, 0);
}
public static boolean getEnableFingerprint() {
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
}
@@ -1168,4 +1168,22 @@ public class Settings {
public static void putPasswdToArchivePasswds(String value) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
}
public static int getListThumbSize() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE;
}
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
LIST_THUMB_SIZE = size;
LIST_THUMB_SIZE_INITED = true;
return size;
}
public static boolean getNotificationRequired() {
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
}
public static void putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
}
}
@@ -61,7 +61,7 @@ public class EhClient {
public EhClient(Context context) {
mRequestThreadPool = IoThreadPoolExecutor.getInstance();
mOkHttpClient = EhApplication.getOkHttpClient(context);
mOkHttpClient = EhApplication.getOkHttpClient();
}
public void execute(EhRequest request) {
@@ -1,95 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* 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.hippo.ehviewer.client;
/*
* Created by Hippo on 2018/3/23.
*/
import android.content.Context;
import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.Hosts;
import com.hippo.ehviewer.Settings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Dns;
public class EhDns implements Dns {
private static final Map<String, List<InetAddress>> builtInHosts;
static {
Map<String, List<InetAddress>> map = new HashMap<>();
put(map, "e-hentai.org", "104.20.134.21", "104.20.135.21", "172.67.0.127");
put(map, "exhentai.org", "178.175.128.252", "178.175.129.252", "178.175.129.254", "178.175.128.254",
"178.175.132.20", "178.175.132.22");
put(map, "repo.e-hentai.org", "94.100.28.57", "94.100.29.73");
put(map, "forums.e-hentai.org", "94.100.18.243");
put(map, "ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt0.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt1.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt2.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "gt3.ehgt.org", "37.48.89.44", "81.171.10.48", "178.162.139.24", "178.162.140.212"
, "2001:1af8:4700:a062:8::47de", "2001:1af8:4700:a062:9::47de", "2001:1af8:4700:a0c9:4::47de", "2001:1af8:4700:a0c9:3::47de");
put(map, "ul.ehgt.org", "94.100.24.82", "94.100.24.72");
put(map, "raw.githubusercontent.com", "151.101.0.133", "151.101.64.133", "151.101.128.133", "151.101.192.133");
builtInHosts = map;
}
private final Hosts hosts;
public EhDns(Context context) {
hosts = EhApplication.getHosts(context);
}
private static void put(Map<String, List<InetAddress>> map, String host, String... ips) {
List<InetAddress> addresses = new ArrayList<>();
for (String ip : ips) {
addresses.add(Hosts.toInetAddress(host, ip));
}
map.put(host, addresses);
}
@NonNull
@Override
public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
List<InetAddress> inetAddresses = hosts.get(hostname);
if (inetAddresses != null) {
return inetAddresses;
}
if (Settings.getBuiltInHosts()) {
inetAddresses = builtInHosts.get(hostname);
if (inetAddresses != null) {
return inetAddresses;
}
}
return SYSTEM.lookup(hostname);
}
}
@@ -0,0 +1,126 @@
/*
* Copyright 2018 Hippo Seven
*
* 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.hippo.ehviewer.client
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.Hosts
import com.hippo.ehviewer.Settings
import okhttp3.Dns
import java.net.InetAddress
import java.net.UnknownHostException
class EhDns : Dns {
private val hosts = EhApplication.hosts
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
init {
/* Pair(ip: String!, blockedByCCP: Boolean!) */
val ehgtHosts = arrayOf(
Pair("37.48.89.44", false),
Pair("81.171.10.48", false),
Pair("178.162.139.24", false),
Pair("178.162.140.212", false),
Pair("2001:1af8:4700:a062:8::47de", false),
Pair("2001:1af8:4700:a062:9::47de", true),
Pair("2001:1af8:4700:a0c9:4::47de", false),
Pair("2001:1af8:4700:a0c9:3::47de", true),
)
put(
builtInHosts,
"e-hentai.org",
Pair("104.20.134.21", true),
Pair("104.20.135.21", false),
Pair("172.67.0.127", false),
)
put(
builtInHosts,
"exhentai.org",
Pair("178.175.128.252", false),
Pair("178.175.129.252", false),
Pair("178.175.129.254", false),
Pair("178.175.128.254", false),
Pair("178.175.132.20", false),
Pair("178.175.132.22", false),
)
put(
builtInHosts,
"repo.e-hentai.org",
Pair("94.100.28.57", true),
Pair("94.100.29.73", true),
)
put(
builtInHosts,
"forums.e-hentai.org",
Pair("94.100.18.243", false),
)
put(
builtInHosts,
"ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt0.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt1.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt2.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"gt3.ehgt.org",
*ehgtHosts
)
put(
builtInHosts,
"ul.ehgt.org",
Pair("94.100.24.82", true),
Pair("94.100.24.72", true),
)
put(
builtInHosts,
"raw.githubusercontent.com",
Pair("151.101.0.133", false),
Pair("151.101.64.133", false),
Pair("151.101.128.133", false),
Pair("151.101.192.133", false),
)
}
private fun put(
map: MutableMap<String, List<InetAddress>>,
host: String,
vararg ips: Pair<String, Boolean>
) {
map[host] = ips.mapNotNull { pair ->
Hosts.toInetAddress(host, pair.first).takeUnless { Settings.getDF() && pair.second }
}
}
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return hosts[hostname] ?: builtInHosts[hostname].takeIf { Settings.getBuiltInHosts() }
?: Dns.SYSTEM.lookup(hostname)
}
}
@@ -245,12 +245,6 @@ public class EhEngine {
}
}
}
if (Settings.getFixThumbUrl()) {
for (GalleryInfo info : list) {
info.thumb = EhUrl.getFixedPreviewThumbUrl(info.thumb);
}
}
}
public static GalleryListParser.Result getGalleryList(@Nullable EhClient.Task task, OkHttpClient okHttpClient,
@@ -367,7 +361,7 @@ public class EhEngine {
body = response.body().string();
String html = EventPaneParser.parse(body);
if (html != null) {
EhApplication.getInstance().showEventPane(html);
EhApplication.getApplication().showEventPane(html);
}
return GalleryDetailParser.parse(body);
} catch (Throwable e) {
@@ -1,79 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class EhSSLSocketFactory extends SSLSocketFactory {
@Override
public String[] getDefaultCipherSuites() {
return ((SSLSocketFactory) getDefault()).getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return ((SSLSocketFactory) getDefault()).getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault();
if (!Settings.getDF()) {
return defaultFactory.createSocket(s, host, port, autoClose);
}
String address = s.getInetAddress().getHostAddress();
SSLSocket socket = (SSLSocket) defaultFactory.createSocket(s, address, port, autoClose);
SSLSession sslSession = socket.getSession();
Log.d("EhSSLSocketFactory", "Host: " + host + " Address: " + address + " Protocol:" + sslSession.getProtocol() + " CipherSuite:" + sslSession.getCipherSuite());
socket.setEnabledProtocols(socket.getSupportedProtocols());
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return getDefault().createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return getDefault().createSocket(address, port, localAddress, localPort);
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client
import android.util.Log
import java.net.InetAddress
import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
class EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> {
return sslSocketFactory.defaultCipherSuites
}
override fun getSupportedCipherSuites(): Array<String> {
return sslSocketFactory.supportedCipherSuites
}
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
val sslSession = socket.session
Log.d(
"EhSSLSocketFactory",
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
)
return socket
}
override fun createSocket(host: String, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
host: String,
port: Int,
localHost: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(host, port, localHost, localPort)
}
override fun createSocket(host: InetAddress, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
address: InetAddress,
port: Int,
localAddress: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
}
}
@@ -260,7 +260,7 @@ public class EhTagDatabase {
}
}
OkHttpClient client = EhApplication.getOkHttpClient(context);
OkHttpClient client = EhApplication.getOkHttpClient();
// Save new sha1
File tempSha1File = new File(dir, sha1Name + ".tmp");
@@ -21,11 +21,6 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.Settings;
import com.hippo.network.UrlBuilder;
import java.util.List;
import java.util.ListIterator;
import okhttp3.HttpUrl;
public class EhUrl {
public static final int SITE_E = 0;
@@ -59,8 +54,6 @@ public class EhUrl {
public static final String ORIGIN_EX = REFERER_EX;
public static final String REFERER_E = "https://" + DOMAIN_E;
public static final String ORIGIN_E = REFERER_E;
private static final String URL_PREFIX_THUMB_E = "https://ehgt.org/";
private static final String URL_PREFIX_THUMB_EX = "https://exhentai.org/t/";
public static String getGalleryDetailUrl(long gid, String token) {
return getGalleryDetailUrl(gid, token, 0, false);
@@ -200,44 +193,4 @@ public class EhUrl {
return URL_WATCHED_EX;
}
}
public static String getThumbUrlPrefix() {
switch (Settings.getGallerySite()) {
default:
//case SITE_E:
return URL_PREFIX_THUMB_E;
//case SITE_EX:
// return URL_PREFIX_THUMB_EX;
}
}
public static String getFixedPreviewThumbUrl(String originUrl) {
HttpUrl url = HttpUrl.parse(originUrl);
if (url == null) return originUrl;
List<String> pathSegments = url.pathSegments();
if (pathSegments.size() < 3) return originUrl;
ListIterator<String> iterator = pathSegments.listIterator(pathSegments.size());
// The last segments, like
// 317a1a254cd9c3269e71b2aa2671fe8d28c91097-260198-640-480-png_250.jpg
if (!iterator.hasPrevious()) return originUrl;
String lastSegment = iterator.previous();
// The second last segments, like
// 7a
if (!iterator.hasPrevious()) return originUrl;
String secondLastSegment = iterator.previous();
// The third last segments, like
// 31
if (!iterator.hasPrevious()) return originUrl;
String thirdLastSegment = iterator.previous();
// Check path segments
if (lastSegment != null && secondLastSegment != null
&& thirdLastSegment != null
&& lastSegment.startsWith(thirdLastSegment)
&& lastSegment.startsWith(secondLastSegment, thirdLastSegment.length())) {
return getThumbUrlPrefix() + thirdLastSegment + "/" + secondLastSegment + "/" + lastSegment;
} else {
return originUrl;
}
}
}
@@ -20,6 +20,7 @@ import android.content.Context;
import android.graphics.Color;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.EhApplication;
@@ -31,10 +32,8 @@ import java.util.regex.Pattern;
public class EhUtils {
public static final int NONE = -1; // Use it for homepage
public static final int UNKNOWN = 0x400;
public static final int ALL_CATEGORY = EhUtils.UNKNOWN - 1;
//DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC;
public static final int PRIVATE = 0x400;
public static final int UNKNOWN = 0x800;
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
public static final int BG_COLOR_MANGA = 0xffff9800;
@@ -67,6 +66,7 @@ public class EhUtils {
EhConfig.ASIAN_PORN,
EhConfig.NON_H,
EhConfig.WESTERN,
PRIVATE,
UNKNOWN};
private static final String[][] CATEGORY_STRINGS = {
@@ -80,6 +80,7 @@ public class EhUtils {
new String[]{"asianporn", "Asian Porn"},
new String[]{"non-h"},
new String[]{"western"},
new String[]{"private"},
new String[]{"unknown"}
};
@@ -94,6 +95,7 @@ public class EhUtils {
return CATEGORY_VALUES[i];
}
@NonNull
public static String getCategory(int type) {
int i;
for (i = 0; i < CATEGORY_VALUES.length - 1; i++) {
@@ -131,14 +133,14 @@ public class EhUtils {
}
public static void signOut(Context context) {
EhApplication.getEhCookieStore(context).signOut();
EhApplication.getEhCookieStore().signOut();
Settings.putAvatar(null);
Settings.putDisplayName(null);
Settings.putNeedSignIn(true);
}
public static boolean needSignedIn(Context context) {
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore(context).hasSignedIn();
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
}
public static String getSuitableTitle(GalleryInfo gi) {
@@ -15,30 +15,23 @@
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client
package com.hippo.ehviewer.client;
import android.annotation.SuppressLint
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
import android.annotation.SuppressLint;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class EhX509TrustManager implements X509TrustManager {
@SuppressLint("CustomX509TrustManager")
class EhX509TrustManager : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
}
@@ -16,16 +16,17 @@
package com.hippo.ehviewer.client.data;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.network.UrlBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FavListUrlBuilder implements Parcelable {
@@ -44,7 +45,9 @@ public class FavListUrlBuilder implements Parcelable {
}
};
private static final String TAG = FavListUrlBuilder.class.getSimpleName();
private int mIndex;
private String mPrev;
private String mNext;
private String mJumpTo;
private String mKeyword;
private int mFavCat = FAV_CAT_ALL;
@@ -52,7 +55,9 @@ public class FavListUrlBuilder implements Parcelable {
}
protected FavListUrlBuilder(Parcel in) {
this.mIndex = in.readInt();
this.mPrev = in.readString();
this.mNext = in.readString();
this.mJumpTo = in.readString();
this.mKeyword = in.readString();
this.mFavCat = in.readInt();
}
@@ -61,8 +66,16 @@ public class FavListUrlBuilder implements Parcelable {
return favCat >= 0 && favCat <= 9;
}
public void setIndex(int index) {
mIndex = index;
public void setPrev(String prev) {
mPrev = prev;
}
public void setNext(String next) {
mNext = next;
}
public void setJumpTo(String jumpTo) {
mJumpTo = jumpTo;
}
public String getKeyword() {
@@ -93,20 +106,30 @@ public class FavListUrlBuilder implements Parcelable {
ub.addQuery("favcat", "all");
}
if (!TextUtils.isEmpty(mKeyword)) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Can't URLEncoder.encode " + mKeyword);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
} else {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8.displayName()));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
}
if (mIndex > 0) {
ub.addQuery("page", Integer.toString(mIndex));
if (mPrev != null && !mPrev.isEmpty()) {
ub.addQuery("prev", mPrev);
}
if (mNext != null && !mNext.isEmpty()) {
ub.addQuery("next", mNext);
}
if (mJumpTo != null && !mJumpTo.isEmpty()) {
ub.addQuery("seek", mJumpTo);
}
return ub.build();
}
@@ -118,7 +141,9 @@ public class FavListUrlBuilder implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mIndex);
dest.writeString(this.mPrev);
dest.writeString(this.mNext);
dest.writeString(this.mJumpTo);
dest.writeString(this.mKeyword);
dest.writeInt(this.mFavCat);
}
@@ -20,6 +20,9 @@ import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import java.util.regex.Pattern;
@@ -108,30 +111,52 @@ public class GalleryInfo implements Parcelable {
return new GalleryInfo[size];
}
};
@PrimaryKey
@ColumnInfo(name = "GID")
public long gid;
@ColumnInfo(name = "TOKEN")
public String token;
@ColumnInfo(name = "TITLE")
public String title;
@ColumnInfo(name = "TITLE_JPN")
public String titleJpn;
@ColumnInfo(name = "THUMB")
public String thumb;
@ColumnInfo(name = "CATEGORY")
public int category;
@ColumnInfo(name = "POSTED")
public String posted;
@ColumnInfo(name = "UPLOADER")
public String uploader;
@Ignore
public boolean disowned;
@ColumnInfo(name = "RATING")
public float rating;
@Ignore
public boolean rated;
@Nullable
@Ignore
public String[] simpleTags;
@Ignore
public int pages;
@Ignore
public int thumbWidth;
@Ignore
public int thumbHeight;
@Ignore
public int spanSize;
@Ignore
public int spanIndex;
@Ignore
public int spanGroupIndex;
/**
* language from title
*/
@ColumnInfo(name = "SIMPLE_LANGUAGE")
public String simpleLanguage;
@Ignore
public int favoriteSlot = -2;
@Ignore
public String favoriteName;
public GalleryInfo() {
@@ -170,6 +195,7 @@ public class GalleryInfo implements Parcelable {
}
private void generateSLangFromTags() {
assert simpleTags != null;
for (String tag : simpleTags) {
for (int i = 0; i < S_LANGS.length; i++) {
if (S_LANG_TAGS[i].equals(tag)) {
@@ -1,636 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.widget.AdvanceSearchTable;
import com.hippo.network.UrlBuilder;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class ListUrlBuilder implements Cloneable, Parcelable {
// Mode
public static final int MODE_NORMAL = 0x0;
public static final int MODE_UPLOADER = 0x1;
public static final int MODE_TAG = 0x2;
public static final int MODE_WHATS_HOT = 0x3;
public static final int MODE_IMAGE_SEARCH = 0x4;
public static final int MODE_SUBSCRIPTION = 0x5;
public static final int MODE_TOPLIST = 0x6;
public static final int DEFAULT_ADVANCE = AdvanceSearchTable.SNAME | AdvanceSearchTable.STAGS;
public static final int DEFAULT_MIN_RATING = 2;
public static final Creator<ListUrlBuilder> CREATOR = new Creator<ListUrlBuilder>() {
@Override
public ListUrlBuilder createFromParcel(Parcel source) {
return new ListUrlBuilder(source);
}
@Override
public ListUrlBuilder[] newArray(int size) {
return new ListUrlBuilder[size];
}
};
@Mode
private int mMode = MODE_NORMAL;
private int mPageIndex = 0;
private int mCategory = EhUtils.NONE;
private String mKeyword = null;
private String mSHash = null;
private int mAdvanceSearch = -1;
private int mMinRating = -1;
private int mPageFrom = -1;
private int mPageTo = -1;
private String mImagePath;
private boolean mUseSimilarityScan;
private boolean mOnlySearchCovers;
private boolean mShowExpunged;
public ListUrlBuilder() {
}
@SuppressWarnings("WrongConstant")
protected ListUrlBuilder(Parcel in) {
this.mMode = in.readInt();
this.mPageIndex = in.readInt();
this.mCategory = in.readInt();
this.mKeyword = in.readString();
this.mAdvanceSearch = in.readInt();
this.mMinRating = in.readInt();
this.mPageFrom = in.readInt();
this.mPageTo = in.readInt();
this.mImagePath = in.readString();
this.mUseSimilarityScan = in.readByte() != 0;
this.mOnlySearchCovers = in.readByte() != 0;
this.mShowExpunged = in.readByte() != 0;
this.mSHash = in.readString();
}
/**
* Make this ListUrlBuilder point to homepage
*/
public void reset() {
mMode = MODE_NORMAL;
mPageIndex = 0;
mCategory = EhUtils.NONE;
mKeyword = null;
mAdvanceSearch = -1;
mMinRating = -1;
mPageFrom = -1;
mPageTo = -1;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
mSHash = null;
}
@Override
public ListUrlBuilder clone() {
try {
return (ListUrlBuilder) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
@Mode
public int getMode() {
return mMode;
}
public void setMode(@Mode int mode) {
mMode = mode;
}
public int getPageIndex() {
return mPageIndex;
}
public void setPageIndex(int pageIndex) {
mPageIndex = pageIndex;
}
public int getCategory() {
return mCategory;
}
public void setCategory(int category) {
mCategory = category;
}
public String getKeyword() {
return MODE_UPLOADER == mMode ? "uploader:" + mKeyword : mKeyword;
}
public void setKeyword(String keyword) {
mKeyword = keyword;
}
public int getAdvanceSearch() {
return mAdvanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
mAdvanceSearch = advanceSearch;
}
public int getMinRating() {
return mMinRating;
}
public void setMinRating(int minRating) {
mMinRating = minRating;
}
public int getPageFrom() {
return mPageFrom;
}
public void setPageFrom(int pageFrom) {
mPageFrom = pageFrom;
}
public int getPageTo() {
return mPageTo;
}
public void setPageTo(int pageTo) {
mPageTo = pageTo;
}
@Nullable
public String getImagePath() {
return mImagePath;
}
public void setImagePath(String imagePath) {
mImagePath = imagePath;
}
public boolean isUseSimilarityScan() {
return mUseSimilarityScan;
}
public void setUseSimilarityScan(boolean useSimilarityScan) {
mUseSimilarityScan = useSimilarityScan;
}
public boolean isOnlySearchCovers() {
return mOnlySearchCovers;
}
public void setOnlySearchCovers(boolean onlySearchCovers) {
mOnlySearchCovers = onlySearchCovers;
}
public boolean isShowExpunged() {
return mShowExpunged;
}
public void setShowExpunged(boolean showExpunged) {
mShowExpunged = showExpunged;
}
/**
* Make them the same
*
* @param lub The template
*/
public void set(ListUrlBuilder lub) {
mMode = lub.mMode;
mPageIndex = lub.mPageIndex;
mCategory = lub.mCategory;
mKeyword = lub.mKeyword;
mAdvanceSearch = lub.mAdvanceSearch;
mMinRating = lub.mMinRating;
mPageFrom = lub.mPageFrom;
mPageTo = lub.mPageTo;
mImagePath = lub.mImagePath;
mUseSimilarityScan = lub.mUseSimilarityScan;
mOnlySearchCovers = lub.mOnlySearchCovers;
mShowExpunged = lub.mShowExpunged;
mSHash = lub.mSHash;
}
public void set(QuickSearch q) {
mMode = q.mode;
mCategory = q.category;
mKeyword = q.keyword;
mAdvanceSearch = q.advanceSearch;
mMinRating = q.minRating;
mPageFrom = q.pageFrom;
mPageTo = q.pageTo;
mImagePath = null;
mUseSimilarityScan = false;
mOnlySearchCovers = false;
mShowExpunged = false;
}
public QuickSearch toQuickSearch() {
QuickSearch q = new QuickSearch();
q.mode = mMode;
q.category = mCategory;
q.keyword = mKeyword;
q.advanceSearch = mAdvanceSearch;
q.minRating = mMinRating;
q.pageFrom = mPageFrom;
q.pageTo = mPageTo;
return q;
}
public boolean equalsQuickSearch(QuickSearch q) {
if (null == q) {
return false;
}
if (q.mode != mMode) {
return false;
}
if (q.category != mCategory) {
return false;
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false;
}
if (q.advanceSearch != mAdvanceSearch) {
return false;
}
if (q.minRating != mMinRating) {
return false;
}
if (q.pageFrom != mPageFrom) {
return false;
}
return q.pageTo == mPageTo;
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
public void setQuery(String query) {
reset();
if (TextUtils.isEmpty(query)) {
return;
}
String[] querys = StringUtils.split(query, '&');
int category = 0;
String keyword = null;
boolean enableAdvanceSearch = false;
int advanceSearch = 0;
boolean enableMinRating = false;
int minRating = -1;
boolean enablePage = false;
int pageFrom = -1;
int pageTo = -1;
for (String str : querys) {
int index = str.indexOf('=');
if (index < 0) {
continue;
}
String key = str.substring(0, index);
String value = str.substring(index + 1);
switch (key) {
case "f_cats":
int cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY);
category |= (~cats) & EhConfig.ALL_CATEGORY;
break;
case "f_doujinshi":
if ("1".equals(value)) {
category |= EhConfig.DOUJINSHI;
}
break;
case "f_manga":
if ("1".equals(value)) {
category |= EhConfig.MANGA;
}
break;
case "f_artistcg":
if ("1".equals(value)) {
category |= EhConfig.ARTIST_CG;
}
break;
case "f_gamecg":
if ("1".equals(value)) {
category |= EhConfig.GAME_CG;
}
break;
case "f_western":
if ("1".equals(value)) {
category |= EhConfig.WESTERN;
}
break;
case "f_non-h":
if ("1".equals(value)) {
category |= EhConfig.NON_H;
}
break;
case "f_imageset":
if ("1".equals(value)) {
category |= EhConfig.IMAGE_SET;
}
break;
case "f_cosplay":
if ("1".equals(value)) {
category |= EhConfig.COSPLAY;
}
break;
case "f_asianporn":
if ("1".equals(value)) {
category |= EhConfig.ASIAN_PORN;
}
break;
case "f_misc":
if ("1".equals(value)) {
category |= EhConfig.MISC;
}
break;
case "f_search":
try {
keyword = URLDecoder.decode(value, "utf-8");
} catch (UnsupportedEncodingException | IllegalArgumentException e) {
// Ignore
}
break;
case "advsearch":
if ("1".equals(value)) {
enableAdvanceSearch = true;
}
break;
case "f_sname":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SNAME;
}
break;
case "f_stags":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STAGS;
}
break;
case "f_sdesc":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDESC;
}
break;
case "f_storr":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STORR;
}
break;
case "f_sto":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.STO;
}
break;
case "f_sdt1":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT1;
}
break;
case "f_sdt2":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SDT2;
}
break;
case "f_sh":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SH;
}
break;
case "f_sfl":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFL;
}
break;
case "f_sfu":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFU;
}
break;
case "f_sft":
if ("on".equals(value)) {
advanceSearch |= AdvanceSearchTable.SFT;
}
break;
case "f_sr":
if ("on".equals(value)) {
enableMinRating = true;
}
break;
case "f_srdd":
minRating = NumberUtils.parseIntSafely(value, -1);
break;
case "f_sp":
if ("on".equals(value)) {
enablePage = true;
}
break;
case "f_spf":
pageFrom = NumberUtils.parseIntSafely(value, -1);
break;
case "f_spt":
pageTo = NumberUtils.parseIntSafely(value, -1);
break;
case "f_shash":
mSHash = value;
break;
}
}
mCategory = category;
mKeyword = keyword;
if (enableAdvanceSearch) {
mAdvanceSearch = advanceSearch;
if (enableMinRating) {
mMinRating = minRating;
} else {
mMinRating = -1;
}
if (enablePage) {
mPageFrom = pageFrom;
mPageTo = pageTo;
} else {
mPageFrom = -1;
mPageTo = -1;
}
} else {
mAdvanceSearch = -1;
}
}
public String build() {
switch (mMode) {
default:
case MODE_NORMAL:
case MODE_SUBSCRIPTION: {
String url;
if (mMode == MODE_NORMAL) {
url = EhUrl.getHost();
} else {
url = EhUrl.getWatchedUrl();
}
UrlBuilder ub = new UrlBuilder(url);
if (mCategory != EhUtils.NONE) {
ub.addQuery("f_cats", (~mCategory) & EhConfig.ALL_CATEGORY);
}
// Search key
if (mKeyword != null) {
String keyword = mKeyword.trim();
if (!keyword.isEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash);
}
// Page index
if (mPageIndex != 0) {
ub.addQuery("page", mPageIndex);
}
// Advance search
if (mAdvanceSearch != -1) {
ub.addQuery("advsearch", "1");
if ((mAdvanceSearch & AdvanceSearchTable.SNAME) != 0)
ub.addQuery("f_sname", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STAGS) != 0)
ub.addQuery("f_stags", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDESC) != 0)
ub.addQuery("f_sdesc", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STORR) != 0)
ub.addQuery("f_storr", "on");
if ((mAdvanceSearch & AdvanceSearchTable.STO) != 0) ub.addQuery("f_sto", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT1) != 0)
ub.addQuery("f_sdt1", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SDT2) != 0)
ub.addQuery("f_sdt2", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SH) != 0) ub.addQuery("f_sh", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFL) != 0) ub.addQuery("f_sfl", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFU) != 0) ub.addQuery("f_sfu", "on");
if ((mAdvanceSearch & AdvanceSearchTable.SFT) != 0) ub.addQuery("f_sft", "on");
// Set min star
if (mMinRating != -1) {
ub.addQuery("f_sr", "on");
ub.addQuery("f_srdd", mMinRating);
}
// Pages
if (mPageFrom != -1 || mPageTo != -1) {
ub.addQuery("f_sp", "on");
ub.addQuery("f_spf", mPageFrom != -1 ? Integer.toString(mPageFrom) : "");
ub.addQuery("f_spt", mPageTo != -1 ? Integer.toString(mPageTo) : "");
}
}
return ub.build();
}
case MODE_UPLOADER: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("uploader/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
return sb.toString();
}
case MODE_TAG: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("tag/");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
return sb.toString();
}
case MODE_WHATS_HOT:
return EhUrl.getPopularUrl();
case MODE_IMAGE_SEARCH:
return EhUrl.getImageSearchUrl();
case MODE_TOPLIST:
StringBuilder sb = new StringBuilder(EhUrl.HOST_E);
sb.append("toplist.php?tl=");
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append("&p=").append(mPageIndex);
}
return sb.toString();
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mMode);
dest.writeInt(this.mPageIndex);
dest.writeInt(this.mCategory);
dest.writeString(this.mKeyword);
dest.writeInt(this.mAdvanceSearch);
dest.writeInt(this.mMinRating);
dest.writeInt(this.mPageFrom);
dest.writeInt(this.mPageTo);
dest.writeString(this.mImagePath);
dest.writeByte(mUseSimilarityScan ? (byte) 1 : (byte) 0);
dest.writeByte(mOnlySearchCovers ? (byte) 1 : (byte) 0);
dest.writeByte(mShowExpunged ? (byte) 1 : (byte) 0);
dest.writeString(this.mSHash);
}
@IntDef({MODE_NORMAL, MODE_UPLOADER, MODE_TAG, MODE_WHATS_HOT, MODE_IMAGE_SEARCH, MODE_SUBSCRIPTION, MODE_TOPLIST})
@Retention(RetentionPolicy.SOURCE)
private @interface Mode {
}
}
@@ -0,0 +1,547 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* 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.hippo.ehviewer.client.data
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.TextUtils
import androidx.annotation.IntDef
import com.hippo.ehviewer.client.EhConfig
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.widget.AdvanceSearchTable
import com.hippo.network.UrlBuilder
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
open class ListUrlBuilder : Cloneable, Parcelable {
@get:Mode
@Mode
var mode = MODE_NORMAL
private var mPrevGid = 0
private var mNextGid = 0
private var mJumpTo: String? = null
var category = EhUtils.NONE
private var mKeyword: String? = null
private var mSHash: String? = null
var advanceSearch = -1
var minRating = -1
var pageFrom = -1
var pageTo = -1
var imagePath: String? = null
var isUseSimilarityScan = false
var isOnlySearchCovers = false
var isShowExpunged = false
constructor()
protected constructor(parcel: Parcel) {
mode = parcel.readInt()
mPrevGid = parcel.readInt()
mNextGid = parcel.readInt()
mJumpTo = parcel.readString()
this.category = parcel.readInt()
mKeyword = parcel.readString()
advanceSearch = parcel.readInt()
minRating = parcel.readInt()
pageFrom = parcel.readInt()
pageTo = parcel.readInt()
imagePath = parcel.readString()
isUseSimilarityScan = parcel.readByte().toInt() != 0
isOnlySearchCovers = parcel.readByte().toInt() != 0
isShowExpunged = parcel.readByte().toInt() != 0
mSHash = parcel.readString()
}
fun reset() {
mode = MODE_NORMAL
mPrevGid = 0
mNextGid = 0
mJumpTo = null
this.category = EhUtils.NONE
mKeyword = null
advanceSearch = -1
minRating = -1
pageFrom = -1
pageTo = -1
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
mSHash = null
}
public override fun clone(): ListUrlBuilder {
return try {
super.clone() as ListUrlBuilder
} catch (e: CloneNotSupportedException) {
throw IllegalStateException(e)
}
}
fun setPrevGid(prevGid: Int) {
mPrevGid = prevGid
}
fun setNextGid(nextGid: Int) {
mNextGid = nextGid
}
fun setJumpTo(jumpTo: String?) {
mJumpTo = jumpTo
}
var keyword: String?
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
set(keyword) {
mKeyword = keyword
}
/**
* Make them the same
*
* @param lub The template
*/
fun set(lub: ListUrlBuilder) {
mode = lub.mode
mPrevGid = lub.mPrevGid
mNextGid = lub.mNextGid
mJumpTo = lub.mJumpTo
this.category = lub.category
mKeyword = lub.mKeyword
advanceSearch = lub.advanceSearch
minRating = lub.minRating
pageFrom = lub.pageFrom
pageTo = lub.pageTo
imagePath = lub.imagePath
isUseSimilarityScan = lub.isUseSimilarityScan
isOnlySearchCovers = lub.isOnlySearchCovers
isShowExpunged = lub.isShowExpunged
mSHash = lub.mSHash
}
fun set(q: QuickSearch) {
mode = q.mode
this.category = q.category
mKeyword = q.keyword
advanceSearch = q.advanceSearch
minRating = q.minRating
pageFrom = q.pageFrom
pageTo = q.pageTo
imagePath = null
isUseSimilarityScan = false
isOnlySearchCovers = false
isShowExpunged = false
}
fun toQuickSearch(): QuickSearch {
val q = QuickSearch()
q.mode = mode
q.category = this.category
q.keyword = mKeyword
q.advanceSearch = advanceSearch
q.minRating = minRating
q.pageFrom = pageFrom
q.pageTo = pageTo
return q
}
fun equalsQuickSearch(q: QuickSearch?): Boolean {
if (null == q) {
return false
}
if (q.mode != mode) {
return false
}
if (q.category != this.category) {
return false
}
if (!StringUtils.equals(q.keyword, mKeyword)) {
return false
}
if (q.advanceSearch != advanceSearch) {
return false
}
if (q.minRating != minRating) {
return false
}
return if (q.pageFrom != pageFrom) {
false
} else q.pageTo == pageTo
}
/**
* @param query xxx=yyy&mmm=nnn
*/
// TODO page
fun setQuery(query: String?) {
reset()
if (TextUtils.isEmpty(query)) {
return
}
val querys = StringUtils.split(query, '&')
var category = 0
var keyword: String? = null
var enableAdvanceSearch = false
var advanceSearch = 0
var enableMinRating = false
var minRating = -1
var enablePage = false
var pageFrom = -1
var pageTo = -1
for (str in querys) {
val index = str.indexOf('=')
if (index < 0) {
continue
}
val key = str.substring(0, index)
val value = str.substring(index + 1)
when (key) {
"f_cats" -> {
val cats = NumberUtils.parseIntSafely(value, EhConfig.ALL_CATEGORY)
category = category or (cats.inv() and EhConfig.ALL_CATEGORY)
}
"f_doujinshi" -> if ("1" == value) {
category = category or EhConfig.DOUJINSHI
}
"f_manga" -> if ("1" == value) {
category = category or EhConfig.MANGA
}
"f_artistcg" -> if ("1" == value) {
category = category or EhConfig.ARTIST_CG
}
"f_gamecg" -> if ("1" == value) {
category = category or EhConfig.GAME_CG
}
"f_western" -> if ("1" == value) {
category = category or EhConfig.WESTERN
}
"f_non-h" -> if ("1" == value) {
category = category or EhConfig.NON_H
}
"f_imageset" -> if ("1" == value) {
category = category or EhConfig.IMAGE_SET
}
"f_cosplay" -> if ("1" == value) {
category = category or EhConfig.COSPLAY
}
"f_asianporn" -> if ("1" == value) {
category = category or EhConfig.ASIAN_PORN
}
"f_misc" -> if ("1" == value) {
category = category or EhConfig.MISC
}
"f_search" -> try {
keyword = URLDecoder.decode(value, "utf-8")
} catch (e: UnsupportedEncodingException) {
// Ignore
} catch (_: IllegalArgumentException) {
}
"advsearch" -> if ("1" == value) {
enableAdvanceSearch = true
}
"f_sh" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SH
}
"f_sto" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.STO
}
"f_sfl" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFL
}
"f_sfu" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFU
}
"f_sft" -> if ("on" == value) {
advanceSearch = advanceSearch or AdvanceSearchTable.SFT
}
"f_sr" -> if ("on" == value) {
enableMinRating = true
}
"f_srdd" -> minRating = NumberUtils.parseIntSafely(value, -1)
"f_sp" -> if ("on" == value) {
enablePage = true
}
"f_spf" -> pageFrom = NumberUtils.parseIntSafely(value, -1)
"f_spt" -> pageTo = NumberUtils.parseIntSafely(value, -1)
"f_shash" -> mSHash = value
}
}
this.category = category
mKeyword = keyword
if (enableAdvanceSearch) {
this.advanceSearch = advanceSearch
if (enableMinRating) {
this.minRating = minRating
} else {
this.minRating = -1
}
if (enablePage) {
this.pageFrom = pageFrom
this.pageTo = pageTo
} else {
this.pageFrom = -1
this.pageTo = -1
}
} else {
this.advanceSearch = -1
}
}
fun build(): String {
return when (mode) {
MODE_NORMAL, MODE_SUBSCRIPTION -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
// Search key
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
mJumpTo?.let {
ub.addQuery("seek", mJumpTo!!)
mPrevGid = 0
mNextGid = 0
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
// Advance search
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
// Set min star
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
// Pages
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
MODE_UPLOADER -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("uploader/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
MODE_TAG -> {
val sb = StringBuilder(EhUrl.getHost())
sb.append("tag/")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mPrevGid != 0) {
sb.append("?prev=").append(mPrevGid)
}
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
MODE_WHATS_HOT -> EhUrl.getPopularUrl()
MODE_IMAGE_SEARCH -> EhUrl.getImageSearchUrl()
MODE_TOPLIST -> {
val sb = StringBuilder(EhUrl.HOST_E)
sb.append("toplist.php?tl=")
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"))
} catch (e: UnsupportedEncodingException) {
// Empty
}
if (mNextGid != 0) {
sb.append("&p=").append(mNextGid)
}
sb.toString()
}
else -> {
val url: String = if (mode == MODE_NORMAL) {
EhUrl.getHost()
} else {
EhUrl.getWatchedUrl()
}
val ub = UrlBuilder(url)
if (this.category != EhUtils.NONE) {
ub.addQuery("f_cats", category.inv() and EhConfig.ALL_CATEGORY)
}
if (mKeyword != null) {
val keyword = mKeyword!!.trim { it <= ' ' }
if (keyword.isNotEmpty()) {
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"))
} catch (_: UnsupportedEncodingException) {
}
}
}
if (mSHash != null) {
ub.addQuery("f_shash", mSHash!!)
}
if (mPrevGid != 0) {
ub.addQuery("prev", mPrevGid)
}
if (mNextGid != 0) {
ub.addQuery("next", mNextGid)
}
if (advanceSearch != -1) {
ub.addQuery("advsearch", "1")
if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery("f_sh", "on")
if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery("f_sto", "on")
if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery("f_sfl", "on")
if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery("f_sfu", "on")
if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery("f_sft", "on")
if (minRating != -1) {
ub.addQuery("f_sr", "on")
ub.addQuery("f_srdd", minRating)
}
if (pageFrom != -1 || pageTo != -1) {
ub.addQuery("f_sp", "on")
ub.addQuery("f_spf", if (pageFrom != -1) pageFrom.toString() else "")
ub.addQuery("f_spt", if (pageTo != -1) pageTo.toString() else "")
}
}
ub.build()
}
}
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(mode)
dest.writeInt(mPrevGid)
dest.writeInt(mNextGid)
dest.writeString(mJumpTo)
dest.writeInt(this.category)
dest.writeString(mKeyword)
dest.writeInt(advanceSearch)
dest.writeInt(minRating)
dest.writeInt(pageFrom)
dest.writeInt(pageTo)
dest.writeString(imagePath)
dest.writeByte(if (isUseSimilarityScan) 1.toByte() else 0.toByte())
dest.writeByte(if (isOnlySearchCovers) 1.toByte() else 0.toByte())
dest.writeByte(if (isShowExpunged) 1.toByte() else 0.toByte())
dest.writeString(mSHash)
}
@IntDef(
MODE_NORMAL,
MODE_UPLOADER,
MODE_TAG,
MODE_WHATS_HOT,
MODE_IMAGE_SEARCH,
MODE_SUBSCRIPTION,
MODE_TOPLIST
)
@Retention(AnnotationRetention.SOURCE)
private annotation class Mode
companion object {
const val MODE_NORMAL = 0x0
const val MODE_UPLOADER = 0x1
const val MODE_TAG = 0x2
const val MODE_WHATS_HOT = 0x3
const val MODE_IMAGE_SEARCH = 0x4
const val MODE_SUBSCRIPTION = 0x5
const val MODE_TOPLIST = 0x6
@JvmField
val CREATOR: Creator<ListUrlBuilder> = object : Creator<ListUrlBuilder> {
override fun createFromParcel(source: Parcel): ListUrlBuilder {
return ListUrlBuilder(source)
}
override fun newArray(size: Int): Array<ListUrlBuilder?> {
return arrayOfNulls(size)
}
}
}
}
@@ -31,18 +31,22 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FavoritesParser {
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+(-\\d+)?)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+(-\\d+)?)");
public static Result parse(String body) throws Exception {
if (body.contains("This page requires you to log on.</p>")) {
throw new EhException(GetText.getString(R.string.need_sign_in));
}
Document d = Jsoup.parse(body);
String[] catArray = new String[10];
int[] countArray = new int[10];
try {
Document d = Jsoup.parse(body);
Element ido = JsoupUtils.getElementByClass(d, "ido");
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
@@ -60,13 +64,24 @@ public class FavoritesParser {
throw new ParseException("Parse favorites error", body);
}
Result re = new Result();
try {
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find()) re.prevPage = matcherPrev.group(1);
if (matcherNext.find()) re.nextPage = matcherNext.group(1);
} catch (Throwable e) {
e.printStackTrace();
}
GalleryListParser.Result result = GalleryListParser.parse(body);
Result re = new Result();
re.catArray = catArray;
re.countArray = countArray;
re.pages = result.pages;
re.nextPage = result.nextPage;
re.galleryInfoList = result.galleryInfoList;
return re;
@@ -75,8 +90,8 @@ public class FavoritesParser {
public static class Result {
public String[] catArray; // Size 10
public int[] countArray; // Size 10
public int pages;
public int nextPage;
public String prevPage;
public String nextPage;
public List<GalleryInfo> galleryInfoList;
}
}
@@ -19,8 +19,6 @@ package com.hippo.ehviewer.client.parser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryComment;
import com.hippo.ehviewer.client.data.GalleryCommentList;
@@ -48,14 +46,12 @@ import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -66,9 +62,6 @@ public class GalleryDetailParser {
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
private static final Pattern PATTERN_TAG_GROUP = Pattern.compile("<tr><td[^<>]+>([\\w\\s]+):</td><td>(?:<div[^<>]+><a[^<>]+>[\\w\\s]+</a></div>)+</td></tr>");
private static final Pattern PATTERN_TAG = Pattern.compile("<div[^<>]+><a[^<>]+>([\\w\\s]+)</a></div>");
private static final Pattern PATTERN_COMMENT = Pattern.compile("<div class=\"c3\">Posted on ([^<>]+) by: &nbsp; <a[^<>]+>([^<>]+)</a>.+?<div class=\"c6\"[^>]*>(.+?)</div><div class=\"c[78]\"");
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
@@ -78,16 +71,13 @@ public class GalleryDetailParser {
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
private static final DateFormat WEB_COMMENT_DATE_FORMAT = new SimpleDateFormat("dd MMMMM yyyy, HH:mm", Locale.US);
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
static {
WEB_COMMENT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public static GalleryDetail parse(String body) throws EhException {
if (body.contains(OFFENSIVE_STRING)) {
throw new OffensiveException();
@@ -188,7 +178,7 @@ public class GalleryDetailParser {
// Uploader
Element gdn = gm.getElementById("gdn");
if (null != gdn) {
gd.disowned = "opacity:0.5".equals(gdn.attr("style"));
gd.disowned = gdn.attr("style").contains("opacity:0.5");
gd.uploader = StringUtils.trim(gdn.text());
} else {
gd.uploader = "";
@@ -198,14 +188,13 @@ public class GalleryDetailParser {
gd.posted = "";
gd.parent = "";
gd.visible = "";
gd.visible = "";
gd.size = "";
gd.pages = 0;
gd.favoriteCount = 0;
try {
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
parseDetailInfo(gd, es.get(i), body);
parseDetailInfo(gd, es.get(i));
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -293,7 +282,7 @@ public class GalleryDetailParser {
}
}
private static void parseDetailInfo(GalleryDetail gd, Element e, String body) {
private static void parseDetailInfo(GalleryDetail gd, Element e) {
Elements es = e.children();
if (es.size() < 2) {
return;
@@ -323,20 +312,16 @@ public class GalleryDetailParser {
}
} else if (key.startsWith("Favorited")) {
switch (value) {
case "Never":
gd.favoriteCount = 0;
break;
case "Once":
gd.favoriteCount = 1;
break;
default:
case "Never" -> gd.favoriteCount = 0;
case "Once" -> gd.favoriteCount = 1;
default -> {
int index = value.indexOf(' ');
if (index == -1) {
gd.favoriteCount = 0;
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
}
break;
}
}
}
}
@@ -377,6 +362,7 @@ public class GalleryDetailParser {
public static GalleryTagGroup[] parseTagGroups(Document document) {
try {
Element taglist = document.getElementById("taglist");
assert taglist != null;
Elements tagGroups = taglist.child(0).child(0).children();
return parseTagGroups(tagGroups);
} catch (Throwable e) {
@@ -396,7 +382,7 @@ public class GalleryDetailParser {
list.add(group);
}
}
return list.toArray(new GalleryTagGroup[list.size()]);
return list.toArray(new GalleryTagGroup[0]);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -404,31 +390,6 @@ public class GalleryDetailParser {
}
}
/**
* Parse tag groups with regular expressions
*/
@NonNull
private static GalleryTagGroup[] parseTagGroups(String body) throws EhException {
List<GalleryTagGroup> list = new LinkedList<>();
Matcher m = PATTERN_TAG_GROUP.matcher(body);
while (m.find()) {
GalleryTagGroup tagGroup = new GalleryTagGroup();
tagGroup.groupName = ParserUtils.trim(m.group(1));
parseGroup(tagGroup, m.group(0));
list.add(tagGroup);
}
return list.toArray(new GalleryTagGroup[list.size()]);
}
private static void parseGroup(GalleryTagGroup tagGroup, String body) {
Matcher m = PATTERN_TAG.matcher(body);
while (m.find()) {
tagGroup.addTag(ParserUtils.trim(m.group(1)));
}
}
@Nullable
@SuppressWarnings("ConstantConditions")
public static GalleryComment parseComment(Element element) {
@@ -446,17 +407,15 @@ public class GalleryDetailParser {
}
for (Element e : c4.children()) {
switch (e.text()) {
case "Vote+":
case "Vote+" -> {
comment.voteUpAble = true;
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Vote-":
}
case "Vote-" -> {
comment.voteDownAble = true;
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Edit":
comment.editable = true;
break;
}
case "Edit" -> comment.editable = true;
}
}
}
@@ -477,7 +436,7 @@ public class GalleryDetailParser {
Element c3 = JsoupUtils.getElementByClass(element, "c3");
String temp = c3.ownText();
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
comment.time = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
// user
comment.user = c3.child(0).text();
// comment
@@ -487,7 +446,7 @@ public class GalleryDetailParser {
if (c8 != null) {
Element e = c8.children().first();
if (e != null) {
comment.lastEdited = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
}
}
return comment;
@@ -505,6 +464,7 @@ public class GalleryDetailParser {
public static GalleryCommentList parseComments(Document document) {
try {
Element cdiv = document.getElementById("cdiv");
assert cdiv != null;
Elements c1s = cdiv.getElementsByClass("c1");
List<GalleryComment> list = new ArrayList<>(c1s.size());
@@ -517,20 +477,21 @@ public class GalleryDetailParser {
Element chd = cdiv.getElementById("chd");
MutableBoolean hasMore = new MutableBoolean(false);
assert chd != null;
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int depth) {
public void head(@NonNull Node node, int depth) {
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
hasMore.value = true;
}
}
@Override
public void tail(Node node, int depth) {
public void tail(@NonNull Node node, int depth) {
}
}, chd);
return new GalleryCommentList(list.toArray(new GalleryComment[list.size()]), hasMore.value);
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -538,38 +499,14 @@ public class GalleryDetailParser {
}
}
/**
* Parse comments with regular expressions
*/
@NonNull
public static GalleryComment[] parseComments(String body) {
List<GalleryComment> list = new LinkedList<>();
Matcher m = PATTERN_COMMENT.matcher(body);
while (m.find()) {
String webDateString = ParserUtils.trim(m.group(1));
Date date;
try {
date = WEB_COMMENT_DATE_FORMAT.parse(webDateString);
} catch (java.text.ParseException e) {
date = new Date(0L);
}
GalleryComment comment = new GalleryComment();
comment.time = date.getTime();
comment.user = ParserUtils.trim(m.group(2));
comment.comment = m.group(3);
list.add(comment);
}
return list.toArray(new GalleryComment[list.size()]);
}
/**
* Parse preview pages with html parser
*/
public static int parsePreviewPages(Document document, String body) throws ParseException {
try {
Elements elements = document.getElementsByClass("ptt").first().child(0).child(0).children();
Element ptt = document.getElementsByClass("ptt").first();
assert ptt != null;
Elements elements = ptt.child(0).child(0).children();
return Integer.parseInt(elements.get(elements.size() - 2).text());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -636,9 +573,10 @@ public class GalleryDetailParser {
try {
LargePreviewSet largePreviewSet = new LargePreviewSet();
Element gdt = d.getElementById("gdt");
assert gdt != null;
Elements gdtls = gdt.getElementsByClass("gdtl");
int n = gdtls.size();
if (n <= 0) {
if (n == 0) {
throw new ParseException("Can't parse large preview", body);
}
for (int i = 0; i < n; i++) {
@@ -646,9 +584,6 @@ public class GalleryDetailParser {
String pageUrl = element.attr("href");
element = element.child(0);
String imageUrl = element.attr("src");
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
int index = Integer.parseInt(element.attr("alt")) - 1;
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -674,9 +609,6 @@ public class GalleryDetailParser {
}
String imageUrl = ParserUtils.trim(m.group(3));
String pageUrl = ParserUtils.trim(m.group(1));
if (Settings.getFixThumbUrl()) {
imageUrl = EhUrl.getFixedPreviewThumbUrl(imageUrl);
}
largePreviewSet.addItem(index, imageUrl, pageUrl);
}
@@ -24,7 +24,6 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryTagGroup;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
@@ -51,7 +50,8 @@ public class GalleryListParser {
private static final Pattern PATTERN_THUMB_SIZE = Pattern.compile("height:(\\d+)px;width:(\\d+)px");
private static final Pattern PATTERN_FAVORITE_SLOT = Pattern.compile("background-color:rgba\\((\\d+),(\\d+),(\\d+),");
private static final Pattern PATTERN_PAGES = Pattern.compile("(\\d+) page");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("page=(\\d+)");
private static final Pattern PATTERN_PREV_PAGE = Pattern.compile("prev=(\\d+)");
private static final Pattern PATTERN_NEXT_PAGE = Pattern.compile("next=(\\d+)");
private static final String[][] FAVORITE_SLOT_RGB = new String[][]{
new String[]{"0", "0", "0"},
@@ -66,16 +66,6 @@ public class GalleryListParser {
new String[]{"224", "128", "224"},
};
private static int parsePages(Document d, String body) throws ParseException {
try {
Elements es = d.getElementsByClass("ptt").first().child(0).child(0).children();
return Integer.parseInt(es.get(es.size() - 2).text().trim());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Can't parse gallery list pages", body);
}
}
private static String parseRating(String ratingStyle) {
Matcher m = PATTERN_RATING.matcher(ratingStyle);
int num1 = Integer.MIN_VALUE;
@@ -146,23 +136,21 @@ public class GalleryListParser {
children = child.children();
}
gi.title = child.text().trim();
Element tbody = JsoupUtils.getElementByTag(glname, "tbody");
if (tbody != null) {
ArrayList<String> tags = new ArrayList<>();
GalleryTagGroup[] groups = GalleryDetailParser.parseTagGroups(tbody.children());
for (GalleryTagGroup group : groups) {
for (int j = 0; j < group.size(); j++) {
tags.add(group.groupName + ":" + group.getTagAt(j));
}
}
gi.simpleTags = tags.toArray(new String[tags.size()]);
}
}
if (gi.title == null) {
return null;
}
// Tags
Elements gts = e.select(".gt, .gtl");
if (gts.size() != 0) {
ArrayList<String> tags = new ArrayList<>();
for (Element gt : gts) {
tags.add(gt.attr("title"));
}
gi.simpleTags = tags.toArray(new String[0]);
}
// Category
gi.category = EhUtils.UNKNOWN;
Element ce = JsoupUtils.getElementByClass(e, "cn");
@@ -266,10 +254,12 @@ public class GalleryListParser {
if (children.size() > uploaderIndex) {
Element div = children.get(uploaderIndex);
if (div != null) {
gi.disowned = "opacity:0.5".equals(div.attr("style"));
gi.disowned = div.attr("style").contains("opacity:0.5");
Element a = div.children().first();
if (a != null) {
gi.uploader = a.text().trim();
} else {
gi.uploader = div.text().trim();
}
}
}
@@ -298,37 +288,33 @@ public class GalleryListParser {
}
public static Result parse(@NonNull String body) throws Exception {
Result result = new Result();
Document d = Jsoup.parse(body);
try {
Element ptt = d.getElementsByClass("ptt").first();
Elements es = ptt.child(0).child(0).children();
result.pages = Integer.parseInt(es.get(es.size() - 2).text().trim());
Element e = es.get(es.size() - 1);
if (e != null) {
e = e.children().first();
if (e != null) {
String href = e.attr("href");
Matcher matcher = PATTERN_NEXT_PAGE.matcher(href);
if (matcher.find()) {
result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0);
}
}
}
Element prev = d.getElementById("uprev");
Element next = d.getElementById("unext");
assert prev != null;
assert next != null;
Matcher matcherPrev = PATTERN_PREV_PAGE.matcher(prev.attr("href"));
Matcher matcherNext = PATTERN_NEXT_PAGE.matcher(next.attr("href"));
if (matcherPrev.find())
result.prevGid = NumberUtils.parseIntSafely(matcherPrev.group(1), 0);
if (matcherNext.find())
result.nextGid = NumberUtils.parseIntSafely(matcherNext.group(1), 0);
result.founds = Integer.MAX_VALUE;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
result.noWatchedTags = body.contains("<p>You do not have any watched tags");
if (body.contains("No hits found</p>")) {
result.pages = 0;
result.founds = 0;
//noinspection unchecked
result.galleryInfoList = Collections.EMPTY_LIST;
return result;
} else if (d.getElementsByClass("ptt").isEmpty()) {
result.pages = 1;
result.founds = 1;
} else {
result.pages = Integer.MAX_VALUE;
result.founds = Integer.MAX_VALUE;
}
}
@@ -350,7 +336,7 @@ public class GalleryListParser {
}
if (list.isEmpty()) {
if (es.size() < 2 || !NO_UNFILTERED_TEXT.equals(es.get(1).text())) {
throw new ParseException("No gallery", body);
Log.d(TAG, "No gallery found");
}
}
result.galleryInfoList = list;
@@ -364,8 +350,9 @@ public class GalleryListParser {
}
public static class Result {
public int pages;
public int nextPage;
public int founds;
public int prevGid;
public int nextGid;
public boolean noWatchedTags;
public List<GalleryInfo> galleryInfoList;
}
@@ -17,6 +17,7 @@
package com.hippo.ehviewer.client.parser;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import com.hippo.ehviewer.client.EhUrl;
@@ -27,6 +28,7 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public final class GalleryListUrlParser {
@@ -91,10 +93,15 @@ public final class GalleryListUrlParser {
uploader = path.substring(prefixLength, index);
}
try {
uploader = URLDecoder.decode(uploader, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8);
} else {
try {
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(uploader)) {
@@ -120,10 +127,15 @@ public final class GalleryListUrlParser {
tag = path.substring(prefixLength, index);
}
try {
tag = URLDecoder.decode(tag, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8);
} else {
try {
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
if (TextUtils.isEmpty(tag)) {
@@ -19,17 +19,18 @@ package com.hippo.ehviewer.client.parser;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class ParserUtils {
public static final DateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
public static final DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC);
public static synchronized String formatDate(long time) {
return sDateFormat.format(new Date(time));
return formatter.format(Instant.ofEpochMilli(time));
}
public static String trim(String str) {
@@ -48,7 +49,4 @@ public class ParserUtils {
return NumberUtils.parseLongSafely(trim(str).replace(",", ""), defValue);
}
public static float parseFloat(String str, float defValue) {
return NumberUtils.parseFloatSafely(trim(str).replace(",", ""), defValue);
}
}
@@ -0,0 +1,9 @@
package com.hippo.ehviewer.dao;
import java.util.List;
public interface BasicDao<T> {
List<T> list();
long insert(T t);
}
@@ -0,0 +1,160 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "BOOKMARKS")
public class BookmarkInfo extends GalleryInfo {
public static final Creator<BookmarkInfo> CREATOR = new Creator<>() {
@Override
public BookmarkInfo createFromParcel(Parcel source) {
return new BookmarkInfo(source);
}
@Override
public BookmarkInfo[] newArray(int size) {
return new BookmarkInfo[size];
}
};
@ColumnInfo(name = "PAGE")
public int page;
@ColumnInfo(name = "TIME")
public long time;
protected BookmarkInfo(Parcel in) {
super(in);
this.page = in.readInt();
this.time = in.readLong();
}
public BookmarkInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public BookmarkInfo(int page, long time) {
this.page = page;
this.time = time;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.page);
dest.writeLong(this.time);
}
}
@@ -0,0 +1,20 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface BookmarksDao extends BasicDao<BookmarkInfo> {
@Insert
long insert(BookmarkInfo bookmark);
@Delete
void delete(BookmarkInfo bookmark);
@Query("SELECT * FROM BOOKMARKS ORDER BY TIME DESC")
List<BookmarkInfo> list();
}
@@ -0,0 +1,30 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "DOWNLOAD_DIRNAME")
public class DownloadDirname {
@PrimaryKey
@ColumnInfo(name = "GID")
private long gid;
@ColumnInfo(name = "DIRNAME")
private String dirname;
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getDirname() {
return dirname;
}
public void setDirname(String dirname) {
this.dirname = dirname;
}
}
@@ -0,0 +1,29 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadDirnameDao extends BasicDao<DownloadDirname> {
@Query("SELECT * FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
DownloadDirname load(long gid);
@Update
void update(DownloadDirname downloadDirname);
@Insert
long insert(DownloadDirname downloadDirname);
@Query("DELETE FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
void deleteByKey(long gid);
@Query("DELETE FROM DOWNLOAD_DIRNAME")
void deleteAll();
@Query("SELECT * FROM DOWNLOAD_DIRNAME")
List<DownloadDirname> list();
}
@@ -0,0 +1,252 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "DOWNLOADS")
public class DownloadInfo extends GalleryInfo {
public static final Creator<DownloadInfo> CREATOR = new Creator<>() {
@Override
public DownloadInfo createFromParcel(Parcel source) {
return new DownloadInfo(source);
}
@Override
public DownloadInfo[] newArray(int size) {
return new DownloadInfo[size];
}
};
public static final int STATE_INVALID = -1;
public static final int STATE_NONE = 0;
public static final int STATE_WAIT = 1;
public static final int STATE_DOWNLOAD = 2;
public static final int STATE_FINISH = 3;
public static final int STATE_FAILED = 4;
@ColumnInfo(name = "STATE")
public int state;
@ColumnInfo(name = "LEGACY")
public int legacy;
@ColumnInfo(name = "TIME")
public long time;
@ColumnInfo(name = "LABEL")
public String label;
@Ignore
public long speed;
@Ignore
public long remaining;
@Ignore
public int finished;
@Ignore
public int downloaded;
@Ignore
public int total;
DownloadInfo() {
}
protected DownloadInfo(Parcel in) {
super(in);
this.state = in.readInt();
this.legacy = in.readInt();
this.time = in.readLong();
this.label = in.readString();
}
public DownloadInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public DownloadInfo(int state, int legacy, long time, String label, long speed, long remaining,
int finished, int downloaded, int total) {
this.state = state;
this.legacy = legacy;
this.time = time;
this.label = label;
this.speed = speed;
this.remaining = remaining;
this.finished = finished;
this.downloaded = downloaded;
this.total = total;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public int getLegacy() {
return legacy;
}
public void setLegacy(int legacy) {
this.legacy = legacy;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.state);
dest.writeInt(this.legacy);
dest.writeLong(this.time);
dest.writeString(this.label);
}
public long getSpeed() {
return this.speed;
}
public void setSpeed(long speed) {
this.speed = speed;
}
public long getRemaining() {
return this.remaining;
}
public void setRemaining(long remaining) {
this.remaining = remaining;
}
public int getFinished() {
return this.finished;
}
public void setFinished(int finished) {
this.finished = finished;
}
public int getDownloaded() {
return this.downloaded;
}
public void setDownloaded(int downloaded) {
this.downloaded = downloaded;
}
public int getTotal() {
return this.total;
}
public void setTotal(int total) {
this.total = total;
}
}
@@ -0,0 +1,40 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "DOWNLOAD_LABELS")
public class DownloadLabel {
@PrimaryKey
@ColumnInfo(name = "_id")
private Long id;
@ColumnInfo(name = "LABEL")
private String label;
@ColumnInfo(name = "TIME")
private long time;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
}
@@ -0,0 +1,30 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadLabelDao extends BasicDao<DownloadLabel> {
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC")
List<DownloadLabel> list();
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC LIMIT :limit OFFSET :offset")
List<DownloadLabel> list(int offset, int limit);
@Update
void update(List<DownloadLabel> downloadLabels);
@Update
void update(DownloadLabel downloadLabel);
@Insert
long insert(DownloadLabel downloadLabel);
@Delete
void delete(DownloadLabel downloadLabel);
}
@@ -0,0 +1,27 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadsDao extends BasicDao<DownloadInfo> {
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME DESC")
List<DownloadInfo> list();
@Query("SELECT * FROM DOWNLOADS WHERE GID = :gid")
DownloadInfo load(long gid);
@Update
void update(DownloadInfo downloadInfo);
@Insert
long insert(DownloadInfo downloadInfo);
@Delete
void delete(DownloadInfo downloadInfo);
}
@@ -0,0 +1,23 @@
package com.hippo.ehviewer.dao;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {BookmarkInfo.class, DownloadInfo.class, DownloadLabel.class, DownloadDirname.class, Filter.class, HistoryInfo.class, LocalFavoriteInfo.class, QuickSearch.class}, version = 4, exportSchema = false)
public abstract class EhDatabase extends RoomDatabase {
public abstract BookmarksDao bookmarksBao();
public abstract DownloadDirnameDao downloadDirnameDao();
public abstract DownloadLabelDao downloadLabelDao();
public abstract DownloadsDao downloadsDao();
public abstract FilterDao filterDao();
public abstract HistoryDao historyDao();
public abstract LocalFavoritesDao localFavoritesDao();
public abstract QuickSearchDao quickSearchDao();
}
@@ -0,0 +1,66 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import com.hippo.util.HashCodeUtils;
import com.hippo.yorozuya.ObjectUtils;
@Entity(tableName = "FILTER")
public class Filter {
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "TEXT")
public String text;
@ColumnInfo(name = "ENABLE")
public Boolean enable;
@PrimaryKey
@ColumnInfo(name = "_id")
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
@Override
public int hashCode() {
return HashCodeUtils.hashCode(mode, text);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Filter filter)) {
return false;
}
return filter.mode == mode && ObjectUtils.equal(filter.text, text);
}
}
@@ -0,0 +1,27 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface FilterDao extends BasicDao<Filter> {
@Query("SELECT * FROM FILTER")
List<Filter> list();
@Update
void update(Filter filter);
@Insert
long insert(Filter filter);
@Delete
void delete(Filter filter);
@Query("SELECT * FROM FILTER WHERE TEXT = :text AND MODE = :mode")
Filter load(String text, int mode);
}
@@ -0,0 +1,40 @@
package com.hippo.ehviewer.dao;
import androidx.paging.PagingSource;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface HistoryDao extends BasicDao<HistoryInfo> {
@Query("SELECT * FROM HISTORY WHERE GID = :gid")
HistoryInfo load(long gid);
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
List<HistoryInfo> list();
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC LIMIT :limit OFFSET :offset")
List<HistoryInfo> list(int offset, int limit);
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
PagingSource<Integer, HistoryInfo> listLazy();
@Update
void update(HistoryInfo historyInfo);
@Insert
long insert(HistoryInfo historyInfo);
@Delete
void delete(HistoryInfo historyInfos);
@Delete
void delete(List<HistoryInfo> historyInfos);
@Query("DELETE FROM HISTORY")
void deleteAll();
}
@@ -0,0 +1,160 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "HISTORY")
public class HistoryInfo extends GalleryInfo {
public static final Creator<HistoryInfo> CREATOR = new Creator<>() {
@Override
public HistoryInfo createFromParcel(Parcel source) {
return new HistoryInfo(source);
}
@Override
public HistoryInfo[] newArray(int size) {
return new HistoryInfo[size];
}
};
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "TIME")
public long time;
protected HistoryInfo(Parcel in) {
super(in);
this.mode = in.readInt();
this.time = in.readLong();
}
public HistoryInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public HistoryInfo(int mode, long time) {
this.mode = mode;
this.time = time;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.mode);
dest.writeLong(this.time);
}
}
@@ -0,0 +1,147 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "LOCAL_FAVORITES")
public class LocalFavoriteInfo extends GalleryInfo {
public static final Creator<LocalFavoriteInfo> CREATOR = new Creator<>() {
@Override
public LocalFavoriteInfo createFromParcel(Parcel source) {
return new LocalFavoriteInfo(source);
}
@Override
public LocalFavoriteInfo[] newArray(int size) {
return new LocalFavoriteInfo[size];
}
};
@ColumnInfo(name = "TIME")
public long time;
protected LocalFavoriteInfo(Parcel in) {
super(in);
this.time = in.readLong();
}
public LocalFavoriteInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public LocalFavoriteInfo() {
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(this.time);
}
}
@@ -0,0 +1,29 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface LocalFavoritesDao extends BasicDao<LocalFavoriteInfo> {
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC")
List<LocalFavoriteInfo> list();
@Query("SELECT * FROM LOCAL_FAVORITES WHERE TITLE LIKE :title ORDER BY TIME DESC")
List<LocalFavoriteInfo> list(String title);
@Query("SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid")
LocalFavoriteInfo load(long gid);
@Insert
long insert(LocalFavoriteInfo localFavoriteInfo);
@Delete
void delete(LocalFavoriteInfo localFavoriteInfo);
@Query("DELETE FROM LOCAL_FAVORITES WHERE GID = :gid")
void deleteByKey(long gid);
}
@@ -0,0 +1,117 @@
package com.hippo.ehviewer.dao;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "QUICK_SEARCH")
public class QuickSearch {
@PrimaryKey
@ColumnInfo(name = "_id")
public Long id;
@ColumnInfo(name = "NAME")
public String name;
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "CATEGORY")
public int category;
@ColumnInfo(name = "KEYWORD")
public String keyword;
@ColumnInfo(name = "ADVANCE_SEARCH")
public int advanceSearch;
@ColumnInfo(name = "MIN_RATING")
public int minRating;
@ColumnInfo(name = "PAGE_FROM")
public int pageFrom;
@ColumnInfo(name = "PAGE_TO")
public int pageTo;
@ColumnInfo(name = "TIME")
public long time;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public int getAdvanceSearch() {
return advanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
this.advanceSearch = advanceSearch;
}
public int getMinRating() {
return minRating;
}
public void setMinRating(int minRating) {
this.minRating = minRating;
}
public int getPageFrom() {
return pageFrom;
}
public void setPageFrom(int pageFrom) {
this.pageFrom = pageFrom;
}
public int getPageTo() {
return pageTo;
}
public void setPageTo(int pageTo) {
this.pageTo = pageTo;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@NonNull
@Override
public String toString() {
return name;
}
}
@@ -0,0 +1,30 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface QuickSearchDao extends BasicDao<QuickSearch> {
@Query("SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC")
List<QuickSearch> list();
@Query("SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC LIMIT :limit OFFSET :offset")
List<QuickSearch> list(int offset, int limit);
@Update
void update(List<QuickSearch> downloadLabels);
@Update
void update(QuickSearch quickSearch);
@Insert
long insert(QuickSearch quickSearch);
@Delete
void delete(QuickSearch quickSearch);
}
@@ -516,7 +516,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
DownloadInfo info = mAllInfoMap.get(gid);
if (info != null) {
// Remove from DB
EhDB.removeDownloadInfo(info.gid);
EhDB.removeDownloadInfo(info);
// Remove all list and map
mAllInfoList.remove(info);
@@ -552,7 +552,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
}
// Remove from DB
EhDB.removeDownloadInfo(info.gid);
EhDB.removeDownloadInfo(info);
// Remove from all info map
mAllInfoList.remove(info);
@@ -1,581 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.ehviewer.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.scene.DownloadsScene;
import com.hippo.scene.StageActivity;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.collect.LongList;
import com.hippo.yorozuya.collect.SparseJBArray;
import com.hippo.yorozuya.collect.SparseJLArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class DownloadService extends Service implements DownloadManager.DownloadListener {
public static final String ACTION_START = "start";
public static final String ACTION_START_RANGE = "start_range";
public static final String ACTION_START_ALL = "start_all";
public static final String ACTION_STOP = "stop";
public static final String ACTION_STOP_RANGE = "stop_range";
public static final String ACTION_STOP_CURRENT = "stop_current";
public static final String ACTION_STOP_ALL = "stop_all";
public static final String ACTION_DELETE = "delete";
public static final String ACTION_DELETE_RANGE = "delete_range";
public static final String ACTION_CLEAR = "clear";
public static final String KEY_GALLERY_INFO = "gallery_info";
public static final String KEY_LABEL = "label";
public static final String KEY_GID = "gid";
public static final String KEY_GID_LIST = "gid_list";
private static final int ID_DOWNLOADING = 1;
private static final int ID_DOWNLOADED = 2;
private static final int ID_509 = 3;
private final static SparseJBArray sItemStateArray = new SparseJBArray();
private final static SparseJLArray<String> sItemTitleArray = new SparseJLArray<>();
private static int sFailedCount;
private static int sFinishedCount;
private static int sDownloadedCount;
@Nullable
private NotificationManagerCompat mNotifyManager;
@Nullable
private DownloadManager mDownloadManager;
private NotificationCompat.Builder mDownloadingBuilder;
private NotificationCompat.Builder mDownloadedBuilder;
private NotificationCompat.Builder m509dBuilder;
private NotificationDelay mDownloadingDelay;
private NotificationDelay mDownloadedDelay;
private NotificationDelay m509Delay;
private String CHANNEL_ID;
public static void clear() {
sFailedCount = 0;
sFinishedCount = 0;
sDownloadedCount = 0;
sItemStateArray.clear();
sItemTitleArray.clear();
}
@Override
public void onCreate() {
super.onCreate();
CHANNEL_ID = getPackageName() + ".download";
mNotifyManager = NotificationManagerCompat.from(this);
mNotifyManager.createNotificationChannel(
new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.download_service))
.build());
mDownloadManager = EhApplication.getDownloadManager(getApplicationContext());
mDownloadManager.setDownloadListener(this);
ensureDownloadingBuilder();
mDownloadingBuilder.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true);
startForeground(ID_DOWNLOADING, mDownloadingBuilder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
mNotifyManager = null;
if (mDownloadManager != null) {
mDownloadManager.setDownloadListener(null);
mDownloadManager = null;
}
mDownloadingBuilder = null;
mDownloadedBuilder = null;
m509dBuilder = null;
if (mDownloadingDelay != null) {
mDownloadingDelay.release();
}
if (mDownloadedDelay != null) {
mDownloadedDelay.release();
}
if (m509Delay != null) {
m509Delay.release();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return START_STICKY;
}
private void handleIntent(Intent intent) {
String action = null;
if (intent != null) {
action = intent.getAction();
}
if (ACTION_START.equals(action)) {
GalleryInfo gi = intent.getParcelableExtra(KEY_GALLERY_INFO);
String label = intent.getStringExtra(KEY_LABEL);
if (gi != null && mDownloadManager != null) {
mDownloadManager.startDownload(gi, label);
}
} else if (ACTION_START_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.startRangeDownload(gidList);
}
} else if (ACTION_START_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.startAllDownload();
}
} else if (ACTION_STOP.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.stopDownload(gid);
}
} else if (ACTION_STOP_CURRENT.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopCurrentDownload();
}
} else if (ACTION_STOP_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.stopRangeDownload(gidList);
}
} else if (ACTION_STOP_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
} else if (ACTION_DELETE.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.deleteDownload(gid);
}
} else if (ACTION_DELETE_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.deleteRangeDownload(gidList);
}
} else if (ACTION_CLEAR.equals(action)) {
clear();
}
checkStopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new IllegalStateException("No bindService");
}
private void ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return;
}
Intent stopAllIntent = new Intent(this, DownloadService.class);
stopAllIntent.setAction(ACTION_STOP_ALL);
PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
.setShowWhen(false)
.setChannelId(CHANNEL_ID);
mDownloadingDelay = new NotificationDelay(this, mNotifyManager, mDownloadingBuilder, ID_DOWNLOADING);
}
private void ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return;
}
Intent clearIntent = new Intent(this, DownloadService.class);
clearIntent.setAction(ACTION_CLEAR);
PendingIntent piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE);
Bundle bundle = new Bundle();
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity);
mDownloadedDelay = new NotificationDelay(this, mNotifyManager, mDownloadedBuilder, ID_DOWNLOADED);
}
private void ensure509Builder() {
if (m509dBuilder != null) {
return;
}
m509dBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)))
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR);
m509Delay = new NotificationDelay(this, mNotifyManager, m509dBuilder, ID_509);
}
@Override
public void onGet509() {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
if (mNotifyManager == null) {
return;
}
ensure509Builder();
m509dBuilder.setWhen(System.currentTimeMillis());
m509Delay.show();
}
@Override
public void onStart(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
Bundle bundle = new Bundle();
bundle.putLong(DownloadsScene.KEY_GID, info.gid);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity);
mDownloadingDelay.startForeground();
}
private void onUpdate(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
long speed = info.speed;
if (speed < 0) {
speed = 0;
}
String text = FileUtils.humanReadableByteCount(speed, false) + "/s";
long remaining = info.remaining;
if (remaining >= 0) {
text = getString(R.string.download_speed_text_2, text, ReadableTime.getShortTimeInterval(remaining));
} else {
text = getString(R.string.download_speed_text, text);
}
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
.setSubText(info.total == -1 || info.finished == -1 ? null : info.finished + "/" + info.total)
.setProgress(info.total, info.finished, false);
mDownloadingDelay.startForeground();
}
@Override
public void onDownload(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onGetPage(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onFinish(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
ensureDownloadedBuilder();
boolean finish = info.state == DownloadInfo.STATE_FINISH;
long gid = info.gid;
int index = sItemStateArray.indexOfKey(gid);
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
sDownloadedCount++;
if (finish) {
sFinishedCount++;
} else {
sFailedCount++;
}
} else { // Contain
boolean oldFinish = sItemStateArray.valueAt(index);
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
if (oldFinish && !finish) {
sFinishedCount--;
sFailedCount++;
} else if (!oldFinish && finish) {
sFinishedCount++;
sFailedCount--;
}
}
String text;
boolean needStyle;
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_succeeded, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount);
needStyle = true;
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_failed, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount);
needStyle = true;
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount);
needStyle = true;
}
NotificationCompat.InboxStyle style;
if (needStyle) {
style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(getString(R.string.stat_download_done_title));
SparseJBArray stateArray = sItemStateArray;
for (int i = 0, n = stateArray.size(); i < n; i++) {
long id = stateArray.keyAt(i);
boolean fin = stateArray.valueAt(i);
String title = sItemTitleArray.get(id);
if (title == null) {
continue;
}
style.addLine(getString(fin ? R.string.stat_download_done_line_succeeded :
R.string.stat_download_done_line_failed, title));
}
} else {
style = null;
}
mDownloadedBuilder.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount);
mDownloadedDelay.show();
checkStopSelf();
}
@Override
public void onCancel(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
checkStopSelf();
}
private void checkStopSelf() {
if (mDownloadManager == null || mDownloadManager.isIdle()) {
stopForeground(true);
stopSelf();
}
}
// TODO Include all notification in one delay
// Avoid frequent notification
private static class NotificationDelay implements Runnable {
private static final int OPS_NOTIFY = 0;
private static final int OPS_CANCEL = 1;
private static final int OPS_START_FOREGROUND = 2;
private static final long DELAY = 1000; // 1s
private final NotificationManagerCompat mNotifyManager;
private final NotificationCompat.Builder mBuilder;
private final int mId;
private Service mService;
private long mLastTime;
private boolean mPosted;
// false for show, true for cancel
@Ops
private int mOps;
public NotificationDelay(Service service, NotificationManagerCompat notifyManager,
NotificationCompat.Builder builder, int id) {
mService = service;
mNotifyManager = notifyManager;
mBuilder = builder;
mId = id;
}
public void release() {
mService = null;
}
public void show() {
if (mPosted) {
mOps = OPS_NOTIFY;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.notify(mId, mBuilder.build());
} else {
// Too quick, post delay
mOps = OPS_NOTIFY;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
mLastTime = now;
}
}
public void cancel() {
if (mPosted) {
mOps = OPS_CANCEL;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId);
} else {
// Too quick, post delay
mOps = OPS_CANCEL;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
public void startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
@Override
public void run() {
mPosted = false;
switch (mOps) {
case OPS_NOTIFY:
mNotifyManager.notify(mId, mBuilder.build());
break;
case OPS_CANCEL:
mNotifyManager.cancel(mId);
break;
case OPS_START_FOREGROUND:
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
break;
}
}
@IntDef({OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND})
@Retention(RetentionPolicy.SOURCE)
private @interface Ops {
}
}
}
@@ -0,0 +1,551 @@
/*
* Copyright 2016 Hippo Seven
*
* 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.hippo.ehviewer.download
import android.Manifest
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.annotation.IntDef
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.scene.StageActivity
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.collect.LongList
import com.hippo.yorozuya.collect.SparseJBArray
import com.hippo.yorozuya.collect.SparseJLArray
class DownloadService : Service(), DownloadManager.DownloadListener {
private var mNotifyManager: NotificationManagerCompat? = null
private var mDownloadManager: DownloadManager? = null
private var mDownloadingBuilder: NotificationCompat.Builder? = null
private var mDownloadedBuilder: NotificationCompat.Builder? = null
private var m509dBuilder: NotificationCompat.Builder? = null
private var mDownloadingDelay: NotificationDelay? = null
private var mDownloadedDelay: NotificationDelay? = null
private var m509Delay: NotificationDelay? = null
private var mChannelID: String? = null
override fun onCreate() {
super.onCreate()
mChannelID = "$packageName.download"
mNotifyManager = NotificationManagerCompat.from(this)
mNotifyManager!!.createNotificationChannel(
NotificationChannelCompat.Builder(
mChannelID!!,
NotificationManagerCompat.IMPORTANCE_LOW
)
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.downloadManager
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())
}
override fun onDestroy() {
super.onDestroy()
mNotifyManager = null
if (mDownloadManager != null) {
mDownloadManager!!.setDownloadListener(null)
mDownloadManager = null
}
mDownloadingBuilder = null
mDownloadedBuilder = null
m509dBuilder = null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
handleIntent(intent)
return START_STICKY
}
private fun handleIntent(intent: Intent?) {
var action: String? = null
if (intent != null) {
action = intent.action
}
if (ACTION_START == action) {
val gi = intent!!.getParcelableExtra<GalleryInfo>(KEY_GALLERY_INFO)
val label = intent.getStringExtra(KEY_LABEL)
if (gi != null && mDownloadManager != null) {
mDownloadManager!!.startDownload(gi, label)
}
} else if (ACTION_START_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.startRangeDownload(gidList)
}
} else if (ACTION_START_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.startAllDownload()
}
} else if (ACTION_STOP == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.stopDownload(gid)
}
} else if (ACTION_STOP_CURRENT == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopCurrentDownload()
}
} else if (ACTION_STOP_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.stopRangeDownload(gidList)
}
} else if (ACTION_STOP_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
} else if (ACTION_DELETE == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.deleteDownload(gid)
}
} else if (ACTION_DELETE_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.deleteRangeDownload(gidList)
}
} else if (ACTION_CLEAR == action) {
clear()
}
checkStopSelf()
}
override fun onBind(intent: Intent): IBinder? {
throw IllegalStateException("No bindService")
}
private fun ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return
}
val stopAllIntent = Intent(this, DownloadService::class.java)
stopAllIntent.action = ACTION_STOP_ALL
val piStopAll =
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
R.drawable.v_pause_x24,
getString(R.string.stat_download_action_stop_all),
piStopAll
)
.setShowWhen(false)
.setChannelId(mChannelID!!)
mDownloadingDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)
}
private fun ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return
}
val clearIntent = Intent(this, DownloadService::class.java)
clearIntent.action = ACTION_CLEAR
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
val bundle = Bundle()
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity)
mDownloadedDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)
}
private fun ensure509Builder() {
if (m509dBuilder != null) {
return
}
m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text))
)
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR)
m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)
}
override fun onGet509() {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
if (mNotifyManager == null) {
return
}
ensure509Builder()
m509dBuilder!!.setWhen(System.currentTimeMillis())
m509Delay!!.show()
}
override fun onStart(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
val bundle = Bundle()
bundle.putLong(DownloadsScene.KEY_GID, info.gid)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity)
mDownloadingDelay!!.startForeground()
}
private fun onUpdate(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
var speed = info.speed
if (speed < 0) {
speed = 0
}
var text = FileUtils.humanReadableByteCount(speed, false) + "/s"
val remaining = info.remaining
text = if (remaining >= 0) {
getString(
R.string.download_speed_text_2,
text,
ReadableTime.getShortTimeInterval(remaining)
)
} else {
getString(R.string.download_speed_text, text)
}
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
.setProgress(info.total, info.finished, false)
mDownloadingDelay!!.startForeground()
}
override fun onDownload(info: DownloadInfo) {
onUpdate(info)
}
override fun onGetPage(info: DownloadInfo) {
onUpdate(info)
}
override fun onFinish(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
ensureDownloadedBuilder()
val finish = info.state == DownloadInfo.STATE_FINISH
val gid = info.gid
val index = sItemStateArray.indexOfKey(gid)
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
sDownloadedCount++
if (finish) {
sFinishedCount++
} else {
sFailedCount++
}
} else { // Contain
val oldFinish = sItemStateArray.valueAt(index)
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
if (oldFinish && !finish) {
sFinishedCount--
sFailedCount++
} else if (!oldFinish && finish) {
sFinishedCount++
sFailedCount--
}
}
val text: String
val needStyle: Boolean
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_succeeded,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount)
needStyle = true
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_failed,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount)
needStyle = true
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount)
needStyle = true
}
val style: NotificationCompat.InboxStyle?
if (needStyle) {
style = NotificationCompat.InboxStyle()
style.setBigContentTitle(getString(R.string.stat_download_done_title))
val stateArray = sItemStateArray
var i = 0
val n = stateArray.size()
while (i < n) {
val id = stateArray.keyAt(i)
val fin = stateArray.valueAt(i)
val title = sItemTitleArray[id]
if (title == null) {
i++
continue
}
style.addLine(
getString(
if (fin) R.string.stat_download_done_line_succeeded else R.string.stat_download_done_line_failed,
title
)
)
i++
}
} else {
style = null
}
mDownloadedBuilder!!.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount)
mDownloadedDelay!!.show()
checkStopSelf()
}
override fun onCancel(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
checkStopSelf()
}
private fun checkStopSelf() {
if (mDownloadManager == null || mDownloadManager!!.isIdle) {
stopForeground(true)
stopSelf()
}
}
private class NotificationDelay(
private var mService: Service,
private val mNotifyManager: NotificationManagerCompat,
private val mBuilder: NotificationCompat.Builder,
private val mId: Int
) : Runnable {
private var mLastTime: Long = 0
private var mPosted = false
// false for show, true for cancel
@Ops
private var mOps = 0
fun show() {
if (mPosted) {
mOps = OPS_NOTIFY
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_NOTIFY
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
mLastTime = now
}
}
fun cancel() {
if (mPosted) {
mOps = OPS_CANCEL
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId)
} else {
// Too quick, post delay
mOps = OPS_CANCEL
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
fun startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mService.startForeground(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
override fun run() {
mPosted = false
when (mOps) {
OPS_NOTIFY -> {
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
}
OPS_CANCEL -> mNotifyManager.cancel(mId)
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
}
}
@IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)
@Retention(AnnotationRetention.SOURCE)
private annotation class Ops
companion object {
private const val OPS_NOTIFY = 0
private const val OPS_CANCEL = 1
private const val OPS_START_FOREGROUND = 2
private const val DELAY: Long = 1000 // 1s
}
}
companion object {
const val ACTION_START = "start"
const val ACTION_START_RANGE = "start_range"
const val ACTION_START_ALL = "start_all"
const val ACTION_STOP = "stop"
const val ACTION_STOP_RANGE = "stop_range"
const val ACTION_STOP_CURRENT = "stop_current"
const val ACTION_STOP_ALL = "stop_all"
const val ACTION_DELETE = "delete"
const val ACTION_DELETE_RANGE = "delete_range"
const val ACTION_CLEAR = "clear"
const val KEY_GALLERY_INFO = "gallery_info"
const val KEY_LABEL = "label"
const val KEY_GID = "gid"
const val KEY_GID_LIST = "gid_list"
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private val sItemStateArray = SparseJBArray()
private val sItemTitleArray = SparseJLArray<String>()
private var sFailedCount = 0
private var sFinishedCount = 0
private var sDownloadedCount = 0
@JvmStatic
fun clear() {
sFailedCount = 0
sFinishedCount = 0
sDownloadedCount = 0
sItemStateArray.clear()
sItemTitleArray.clear()
}
}
}
@@ -17,6 +17,7 @@
package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.graphics.ImageDecoder;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
@@ -24,20 +25,20 @@ import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.Native;
import com.hippo.UriArchiveAccessor;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.glgallery.GalleryPageView;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -52,11 +53,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private final LinkedHashMap<Integer, Long> streams = new LinkedHashMap<>();
private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private final LinkedHashMap<Integer, ByteBuffer> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private Thread archiveThread;
private Thread decodeThread;
private volatile int size = STATE_WAIT;
private String error;
@@ -78,25 +82,22 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
int id = sIdGenerator.incrementAndGet();
archiveThread = new PriorityThread(
new ArchiveTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
archiveThread.start();
decodeThread = new PriorityThread(
new DecodeTask(), "DecodeTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
decodeThread.start();
for (Thread i : decodeThread) {
i.start();
}
}
@Override
public void stop() {
super.stop();
if (archiveThread != null) {
archiveThread.interrupt();
archiveThread = null;
}
if (decodeThread != null) {
decodeThread.interrupt();
decodeThread = null;
for (Thread i : decodeThread) {
i.interrupt();
}
try {
archiveAccessor.close();
@@ -114,11 +115,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
protected void onRequest(int index) {
boolean inDecodeTask;
synchronized (streams) {
inDecodeTask = streams.containsKey(index) || index == decodingIndex.get();
inDecodeTask = streams.containsKey(index);
}
synchronized (requests) {
boolean inArchiveTask = requests.contains(index) || index == extractingIndex.get();
boolean inArchiveTask = requests.contains(index);
if (!inArchiveTask && !inDecodeTask) {
requests.add(index);
requests.notify();
@@ -147,31 +148,53 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
@NonNull
@Override
public String getImageFilename(int index) {
// TODO
return Integer.toString(index);
}
@Override
public boolean save(int index, @NonNull UniFile file) {
// TODO
return false;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
// TODO
return null;
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
// TODO
return Integer.toString(index);
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
}
private class ArchiveTask implements Runnable {
@Override
public boolean save(int index, @NonNull UniFile file) {
int fd;
FileOutputStream stream;
try {
stream = (FileOutputStream) file.openOutputStream();
fd = Native.getFd(stream.getFD());
} catch (IOException e) {
e.printStackTrace();
return false;
}
archiveAccessor.extractToFd(index, fd);
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
save(index, dst);
return dst;
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask())
};
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
@@ -229,7 +252,26 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
}
for (Thread i : archiveThreads) {
i.start();
}
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
for (Thread i : archiveThreads) {
i.interrupt();
}
}
}
}
}
private class ArchiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
@@ -243,12 +285,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
continue;
}
index = requests.pop();
extractingIndex.lazySet(index);
}
// Check index valid
if (index < 0 || index >= size) {
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
continue;
}
@@ -259,10 +299,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
long addr = archiveAccessor.extracttoOutputStream(index);
ByteBuffer buffer = archiveAccessor.extractToByteBuffer(index);
synchronized (streams) {
streams.put(index, addr);
streams.put(index, buffer);
streams.notify();
}
}
@@ -274,35 +314,40 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Long addr;
ByteBuffer buffer;
synchronized (streams) {
if (streams.isEmpty()) {
try {
streams.wait();
} catch (InterruptedException e) {
// Interrupted
break;
}
continue;
}
Iterator<Map.Entry<Integer, Long>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Long> entry = iterator.next();
Iterator<Map.Entry<Integer, ByteBuffer>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, ByteBuffer> entry = iterator.next();
iterator.remove();
index = entry.getKey();
addr = entry.getValue();
decodingIndex.lazySet(index);
buffer = entry.getValue();
}
try {
Image image = Image.decodeAddr(addr, true);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
Image image = null;
if (buffer != null) {
try {
image = Image.decode(buffer, () -> {
archiveAccessor.releaseByteBuffer(buffer);
return null;
});
} catch (ImageDecoder.DecodeException e) {
archiveAccessor.releaseByteBuffer(buffer);
e.printStackTrace();
}
} finally {
decodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
}
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
}
}
@@ -259,13 +259,9 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable {
InputStream is = null;
try {
is = files[index].openInputStream();
Image image = Image.decode(((FileInputStream)is).getFD(), true);
Image image = Image.decode((FileInputStream) is);
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
notifyPageSucceed(index, image);
} catch (IOException e) {
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_reading_failed));
@@ -0,0 +1,93 @@
/*
* Copyright 2018 Hippo Seven
*
* 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.hippo.ehviewer.preference
import android.content.Context
import android.content.DialogInterface
import android.text.Html
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.preference.MessagePreference
import com.hippo.util.addTextToClipboard
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.LinkedList
class AccountPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MessagePreference(context, attrs) {
private val mActivity = context as SettingsActivity
private var message: String? = null
init {
val store = EhApplication.ehCookieStore
val eCookies = store.getCookies(EhUrl.HOST_E.toHttpUrl())
val exCookies = store.getCookies(EhUrl.HOST_EX.toHttpUrl())
val cookies: MutableList<Cookie> = LinkedList(eCookies)
cookies.addAll(exCookies)
var ipbMemberId: String? = null
var ipbPassHash: String? = null
var igneous: String? = null
var i = 0
val n = cookies.size
while (i < n) {
val cookie = cookies[i]
when (cookie.name) {
EhCookieStore.KEY_IPD_MEMBER_ID -> ipbMemberId = cookie.value
EhCookieStore.KEY_IPD_PASS_HASH -> ipbPassHash = cookie.value
EhCookieStore.KEY_IGNEOUS -> igneous = cookie.value
}
i++
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = (EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous)
setDialogMessage(
Html.fromHtml(
context.getString(
R.string.settings_eh_identity_cookies_signed,
message
), Html.FROM_HTML_MODE_LEGACY
)
)
message = message!!.replace("<br>", "\n")
} else {
setDialogMessage(context.getString(R.string.settings_eh_identity_cookies_tourist))
}
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
if (message != null) {
builder.setNeutralButton(R.string.settings_eh_identity_cookies_copy) { dialog: DialogInterface?, which: Int ->
mActivity.addTextToClipboard(message, true)
this@AccountPreference.onClick(dialog, which)
}
}
builder.setPositiveButton(R.string.settings_eh_sign_out) { _: DialogInterface?, _: Int ->
EhUtils.signOut(context)
mActivity.showTip(R.string.settings_eh_sign_out_tip, BaseScene.LENGTH_SHORT)
}
}
}
@@ -55,7 +55,7 @@ public class CleanRedundancyPreference extends TaskPreference {
public ClearTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager(context.getApplicationContext());
mManager = EhApplication.getDownloadManager();
}
// True for cleared
@@ -1,117 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* 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.hippo.ehviewer.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.util.AttributeSet;
import androidx.appcompat.app.AlertDialog;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.preference.MessagePreference;
import com.hippo.util.ClipboardUtil;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class IdentityCookiePreference extends MessagePreference {
@SuppressLint("StaticFieldLeak")
private final SettingsActivity mActivity;
private String message;
public IdentityCookiePreference(Context context) {
super(context);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivity = (SettingsActivity) context;
init();
}
private void init() {
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
List<Cookie> eCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_E));
List<Cookie> exCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_EX));
List<Cookie> cookies = new LinkedList<>(eCookies);
cookies.addAll(exCookies);
String ipbMemberId = null;
String ipbPassHash = null;
String igneous = null;
for (int i = 0, n = cookies.size(); i < n; i++) {
Cookie cookie = cookies.get(i);
switch (cookie.name()) {
case EhCookieStore.KEY_IPD_MEMBER_ID:
ipbMemberId = cookie.value();
break;
case EhCookieStore.KEY_IPD_PASS_HASH:
ipbPassHash = cookie.value();
break;
case EhCookieStore.KEY_IGNEOUS:
igneous = cookie.value();
break;
}
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous;
setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message), Html.FROM_HTML_MODE_LEGACY));
message = message.replace("<br>", "\n");
} else {
setDialogMessage(getContext().getString(R.string.settings_eh_identity_cookies_tourist));
}
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
if (message != null) {
builder.setPositiveButton(R.string.settings_eh_identity_cookies_copy, (dialog, which) -> {
ClipboardUtil.addTextToClipboard(message);
mActivity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT);
IdentityCookiePreference.this.onClick(dialog, which);
});
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setPositiveButton(android.R.string.ok, null);
}
}
}
@@ -161,7 +161,7 @@ class ProxyPreference @JvmOverloads constructor(
Settings.putProxyIp(ip)
Settings.putProxyPort(port)
updateSummary(type, ip, port)
EhApplication.getEhProxySelector(getContext()).updateProxy()
EhApplication.ehProxySelector.updateProxy()
dialog.dismiss()
}
}
@@ -74,8 +74,8 @@ public class RestoreDownloadPreference extends TaskPreference {
public RestoreTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager(context.getApplicationContext());
mHttpClient = EhApplication.getOkHttpClient(context.getApplicationContext());
mManager = EhApplication.getDownloadManager();
mHttpClient = EhApplication.getOkHttpClient();
}
private RestoreItem getRestoreItem(UniFile file) {
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.util.WebViewUtil;
import okhttp3.HttpUrl;
@@ -59,11 +58,7 @@ public class SignInRequiredPreference extends Preference implements Preference.O
@Override
public boolean onPreferenceClick(Preference preference) {
if (!WebViewUtil.available(getContext())) {
mActivity.showTip(R.string.error_no_webview, BaseScene.LENGTH_SHORT);
return true;
}
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
EhCookieStore store = EhApplication.getEhCookieStore();
HttpUrl e = HttpUrl.get(EhUrl.HOST_E);
HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX);
Loaded 100 of 366 files, more files were not shown because too many files have changed in this diff. Show more