Compare commits

...
837 Commits
Author SHA1 Message Date
tarsin 4512f2a375 Bump Versio
Releases / Build (push) Has been cancelled
2023-01-20 18:07:19 +08:00
tarsin 68442afa0f Update coil 2023-01-20 12:31:46 +08:00
tarsin ddf4d0cbc3 Use our modified coil temporarily 2023-01-20 10:59:38 +08:00
FooIbar 4be185e428 Resolve #696 2023-01-19 21:30:24 +08:00
tarsin f75e66ac19 Upgrade compose compiler 2023-01-18 21:21:40 +08:00
tarsin abda781f02 Cleanup code 2023-01-18 18:50:38 +08:00
tarsin c1533ec66d Avoid unnecessary lambda object create 2023-01-18 18:23:46 +08:00
tarsin c8319446b0 Bump Version
Releases / Build (push) Has been cancelled
2023-01-18 17:44:45 +08:00
tarsin 7ae452cf13 Fixup! Use FileChannel transferFrom 2023-01-18 12:29:19 +08:00
tarsin 4c024c5faa Fixup! Use FileChannel transferFrom 2023-01-18 12:24:10 +08:00
tarsin 9d16e9d98c Use FileChannel transferFrom, Resolve #682 2023-01-18 12:18:43 +08:00
tarsin 48357d08f7 Add TODOs 2023-01-18 10:47:15 +08:00
tarsin 8e0bcf40db Rewrite the GIF frame delay if it's below a threshold, reland, Resolve #641 2023-01-18 10:46:21 +08:00
renovate[bot] 85ef5f04dd Update plugin com.android.application to v8.0.0-beta01 2023-01-18 09:08:12 +08:00
tarsin 18e0748d14 Revert "Update dependency gradle to v8.0-rc-2"
This reverts commit cdde5b098e.
2023-01-18 09:08:01 +08:00
renovate[bot] cdde5b098e Update dependency gradle to v8.0-rc-2 2023-01-17 21:19:03 +08:00
FooIbar f83964752d Set gallery site to eh when sign out 2023-01-17 16:49:16 +08:00
tarsin 0c3e34bbf9 Fix type warning 2023-01-17 10:50:56 +08:00
tarsin 6dea7faac8 Introduce gif rewriter stub 2023-01-17 10:50:11 +08:00
tarsin 726f482c33 Introduce new file read path 2023-01-17 10:40:20 +08:00
tarsin cb242f664a Convert Archive load to ByteBufferSource path 2023-01-17 09:29:44 +08:00
tarsin edc0345970 Introduce out image source type 2023-01-17 09:15:48 +08:00
tarsin a50535dd76 Fix crash 2023-01-17 09:00:10 +08:00
tarsin 505942aa25 Explicitly depend latest okio 2023-01-17 08:56:05 +08:00
tarsin 38a593a9de Resolve #686 2023-01-17 08:51:51 +08:00
tarsin da87fb2551 Fix image save 2023-01-17 08:20:57 +08:00
tarsin d4dc65a4ba Drop old disklrucache (#685)
* Drop Old DiskLruCache

* Avoid being interrupted when completing editor

* Protect all journal operations

* Revert "Protect all journal operations"

This reverts commit 5b7c7e3b33a515ee3e92826a6b76c2dfb3040c5d.

* Revert "Avoid being interrupted when completing editor"

This reverts commit cf4fbed1281eff766c6e65b6f685dfc355208a0b.

* Shutdown Thread gracefully

Test: m, Check no thread leaks

* Fix download extensions
2023-01-16 21:44:25 +08:00
Edward Wu 53463caedd Ehviewer: Translate: Update zh-rTW Translate (01/06)
* Update strings translate
* Reformat to be consistent with Simplified Chinese xml

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2023-01-16 18:34:37 +08:00
tarsin 0707a0fad8 Report DiskLruCache corruption if met 2023-01-16 17:42:55 +08:00
tarsin dd1024af27 Use Tonal Button for register 2023-01-16 17:38:24 +08:00
FooIbar 7edc58a6da Get funds from GP Exchange when unavailable from the archive page 2023-01-16 17:08:03 +08:00
FooIbar 5a5da0cace Convert EhUrl to kotlin 2023-01-16 17:08:03 +08:00
FooIbar 53f02ed366 Rename .java to .kt 2023-01-16 17:08:03 +08:00
tarsin 27cfa885e7 Resolve #681 2023-01-16 12:11:47 +08:00
tarsin beb132d604 Revert "Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)"
This reverts commit 0ebb55551c.
2023-01-16 11:18:06 +08:00
tarsin b1b10298b4 Revert "Avoid unnecessary overhead"
This reverts commit eada1bc023.
2023-01-16 11:17:53 +08:00
tarsin b1993ff7d2 Fix Gif LruCache size 2023-01-16 10:56:02 +08:00
FooIbar 2afe5fe5b2 Show current funds in archive dialog 2023-01-15 22:19:41 +08:00
FooIbar 52fa8f64cc Convert ArchiveParser to kotlin 2023-01-15 22:19:41 +08:00
FooIbar 4f7978070f Rename .java to .kt 2023-01-15 22:19:41 +08:00
tarsin a909983822 Don't exclude compose metadata 2023-01-15 16:45:43 +08:00
小白-白 5267cad896 Fix wrong extension when downloading 2023-01-15 14:34:03 +08:00
小白-白 ce274cf65b Hide unavailable archive 2023-01-15 14:34:03 +08:00
tarsin 55b6b2e34d Cleanup res 2023-01-15 13:29:18 +08:00
tarsin 5e4e2971b5 Fix code style 2023-01-15 13:24:09 +08:00
tarsin a933bcfb1e Resolve #677 2023-01-15 13:23:05 +08:00
tarsin b4b38e27f3 Resolve #675 2023-01-15 13:17:13 +08:00
tarsin bc70824b57 Fixup! Fix stuck when loading preview 2023-01-15 12:55:05 +08:00
tarsin 87ecaaa6a4 Fixup! Use viewLifecycleOwner 2023-01-15 12:52:21 +08:00
tarsin c4cd9cd802 Fix stuck when loading preview 2023-01-15 12:49:18 +08:00
tarsin bbe0e8c5a9 Remove preview limit
At least it works well on my MT6765
2023-01-15 12:42:04 +08:00
tarsin 3f8984d2dc Resolve #668 2023-01-15 10:50:55 +08:00
tarsin f695c4598a Bump Version
Releases / Build (push) Has been cancelled
2023-01-15 09:57:00 +08:00
tarsin eada1bc023 Avoid unnecessary overhead 2023-01-14 19:56:55 +08:00
FooIbar 0ebb55551c Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783) (#672)
* Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)

Resolve #641

* fixup! Rewrite the GIF frame delay if it's below a threshold (coil-kt/coil#783)
2023-01-14 17:19:48 +08:00
tarsin 20bce419c9 Fix GalleryDetailScene bindViewFirst 2023-01-14 12:19:58 +08:00
tarsin ca3fc370c3 Minor refactor 2023-01-14 12:11:27 +08:00
tarsin 94a8b78d1a Use typesafe request 2023-01-14 12:03:55 +08:00
tarsin 06e9ff6fcf Add comments for some dependencies 2023-01-14 11:49:29 +08:00
FooIbar 699e74656d Disable drag and swipe on "All" or "Default" label 2023-01-14 11:41:51 +08:00
tarsin b3ad6ef8b8 Move out DirectionalViewPager
We rewrite it with ViewPager2 later
2023-01-14 11:33:26 +08:00
tarsin 673c8e25f5 Cleanup dependencies 2023-01-14 11:27:05 +08:00
tarsin 333c8e812d Remove lifecycle annotation
We turned to viewbinding
2023-01-14 10:34:44 +08:00
tarsin f0f7a88cfb Kotlinize EhFilter 2023-01-14 10:28:47 +08:00
tarsin 0661d78bce Rename .java to .kt 2023-01-14 10:28:47 +08:00
tarsin dc4aece272 Inline EhCookieStore supertype 2023-01-14 10:21:59 +08:00
tarsin 57a312b0b4 Singletonize EhCookieStore 2023-01-14 10:11:49 +08:00
tarsin b9897ed6ac Rename .java to .kt 2023-01-14 10:11:49 +08:00
tarsin 733230f037 Convert EhCallback to kotlin 2023-01-14 10:04:55 +08:00
tarsin fc6ba3e6ad Rename .java to .kt 2023-01-14 10:04:55 +08:00
tarsin 2d7c539c5f Convert FavouriteScene to kotlin 2023-01-14 09:55:11 +08:00
tarsin 7f34965026 Rename .java to .kt 2023-01-14 09:55:11 +08:00
tarsin de8b915cff Convert GalleryAdapter to kotlin 2023-01-14 09:32:15 +08:00
tarsin d56040581d Rename .java to .kt 2023-01-14 09:32:15 +08:00
tarsin 9869b7bbb5 Convert GalleryCommentsScene to kotlin 2023-01-14 09:26:56 +08:00
tarsin 7df97c28c0 Rename .java to .kt 2023-01-14 09:26:56 +08:00
tarsin 018a559f78 Fix crash in grid mode 2023-01-13 16:49:52 +08:00
tarsin 3b32ad5fed Don't refresh fragment when reselected 2023-01-13 10:26:41 +08:00
tarsin f796d2a842 Tweak NavigationUI to disable multiple backstack and use custom anim 2023-01-13 10:13:10 +08:00
FooIbar 2f7e84e126 Refresh the item as well if the confirmation dialog is canceled 2023-01-12 17:31:28 +08:00
FooIbar 996ddbf3db fixup! Support drag reordering for downloads 2023-01-12 16:48:30 +08:00
FooIbar 7779ba2ac5 Add "All" in download labels
Resolve #412
2023-01-12 15:23:12 +08:00
tarsin 4dadeab762 Avoid querying database on Main Thread, GalleryDetailScene, #629 2023-01-12 12:06:10 +08:00
tarsin f4d3131ef3 Put history in IO coroutines, #629 2023-01-12 11:53:41 +08:00
renovate[bot] efa2ac803e Update dependency androidx.compose:compose-bom to v2023 2023-01-12 11:50:46 +08:00
renovate[bot] e0be9d46bd Update room_version to v2.5.0 2023-01-12 11:50:32 +08:00
tarsin 106b27dbec Finalize ViewBindingize GalleryDetailScene 2023-01-12 11:46:56 +08:00
tarsin f764567523 ViewBindingize GalleryDetailScene, step 6 2023-01-12 11:40:56 +08:00
tarsin 245f8669bc ViewBindingize GalleryDetailScene, step 5 2023-01-12 11:34:51 +08:00
tarsin b265d9bba6 ViewBindingize GalleryDetailScene, step 4 2023-01-12 11:28:12 +08:00
tarsin 72228574f9 ViewBindingize GalleryDetailScene, step 3 2023-01-12 11:16:32 +08:00
tarsin fba1ac3d90 ViewBindingize GalleryDetailScene, step 2 2023-01-12 11:11:32 +08:00
tarsin 4a73fb565e ViewBindingize GalleryDetailScene, step 1 2023-01-12 11:05:41 +08:00
tarsin 13011ed4af Kotlinize GalleryDetailScene 2023-01-12 10:32:49 +08:00
tarsin b2eccdefd3 Rename .java to .kt 2023-01-12 10:32:49 +08:00
tarsin dd2d6a977b Kotlinize GalleryHolder 2023-01-12 10:01:07 +08:00
tarsin 38cd1b6040 Rename .java to .kt 2023-01-12 10:01:07 +08:00
tarsin 1a28bbcde8 ViewBindinglize GalleryPreviewsScene 2023-01-12 09:57:54 +08:00
tarsin 91013daadc Kotlinize GalleryPreviewsScene 2023-01-12 09:50:09 +08:00
tarsin 1ad435e79d Rename .java to .kt 2023-01-12 09:50:09 +08:00
tarsin fd5c300c53 Remove unused navigateToTop 2023-01-12 09:39:57 +08:00
tarsin aa7d9cf29c Remove unused TransitionNameFactory 2023-01-12 09:38:14 +08:00
tarsin 8db1c3400e Kotlinize ProgressScene 2023-01-12 09:34:48 +08:00
tarsin e723336706 Rename .java to .kt 2023-01-12 09:34:48 +08:00
tarsin b5cd4c36ab Singletonize ItemTouchHelper 2023-01-12 09:22:35 +08:00
tarsin d1928f2647 Fixup! Support drag reordering for downloads, fix drag ripple 2023-01-12 09:12:37 +08:00
tarsin 61621ae00b Use compatible compose compiler 2023-01-12 08:56:46 +08:00
tarsin ac20755c45 Bump Version
Releases / Build (push) Has been cancelled
2023-01-12 08:48:14 +08:00
renovate[bot] 610202bbbd Update dependency androidx.startup:startup-runtime to v1.2.0-alpha02 2023-01-12 07:23:13 +08:00
renovate[bot] 58ce7134e1 Update lifecycle_version to v2.6.0-alpha04 2023-01-12 07:22:59 +08:00
FooIbar 71f91cd79e Get updated positions in click listeners
Resolve #655
2023-01-12 07:22:32 +08:00
tarsin 8c821ecad3 Make EhEngine obtain its okhttpClient 2023-01-11 21:46:17 +08:00
FooIbar c704940462 Support drag reordering for downloads
Resolve #54
2023-01-11 21:31:18 +08:00
tarsin 6a7cd1b89a Add some comments and TODO 2023-01-11 14:35:43 +08:00
tarsin e29da0ba13 Cleanup res 2023-01-11 14:28:53 +08:00
tarsin 7791fa10f5 Getprofile in SelectSiteScreen 2023-01-11 14:11:36 +08:00
tarsin 48ed3c8f31 Avoid webview reentry 2023-01-11 14:07:44 +08:00
tarsin 7ad7a9b94e Add WebviewSignInScreen 2023-01-11 13:59:28 +08:00
tarsin 54b929ba24 Use LocalClipboardManager to better support multiplatform 2023-01-11 13:28:00 +08:00
FooIbar 7b655c6260 Show dialog before removing quick search 2023-01-11 13:24:33 +08:00
FooIbar 4e8d41b145 Show dialog before removing download label 2023-01-11 13:24:33 +08:00
tarsin f036774e07 Remove origin signin scene 2023-01-11 13:22:56 +08:00
tarsin 97d1bd7685 Start moving to new compose login activity 2023-01-11 13:12:16 +08:00
tarsin 8e39449856 Restore finish from SelectSiteScreen 2023-01-11 13:06:27 +08:00
tarsin 30f1a5a6eb Add SelectSiteScreen Impl 2023-01-11 13:01:46 +08:00
tarsin 627abb047c Compose dependency tweaks 2023-01-11 12:07:33 +08:00
tarsin 9284008afe Refine CookieSignInScreen 2023-01-11 11:44:13 +08:00
tarsin 9159f4616f Refine SignInScreen KeyBoard options 2023-01-11 10:41:42 +08:00
tarsin 73296b1430 Refine SignInScreen TextInput 2023-01-11 10:25:43 +08:00
tarsin c74e622d69 Add snackbar to CookieSignInScreen layout 2023-01-11 09:55:41 +08:00
tarsin f48eb55516 Basic CookieSignInScreen layout 2023-01-11 09:23:10 +08:00
tarsin b01f6ef8d3 Fixup! use local functions 2023-01-11 08:27:46 +08:00
tarsin 167185ef45 Basic signin refine 2023-01-11 07:58:40 +08:00
tarsin 777a8ffeaa Refine SignInScreen 2023-01-10 20:58:28 +08:00
FooIbar b2b3e6c485 Add tips for torrent download 2023-01-10 20:30:26 +08:00
tarsin 613d502fc1 Use Mdc3Theme 2023-01-10 20:26:39 +08:00
tarsin 79da457db4 Basic signin Compose layout 2023-01-10 20:21:35 +08:00
tarsin b1a431ed82 Use mca component to judge is dynamic color available 2023-01-10 18:17:10 +08:00
tarsin b0f2776a23 Cleanup compose theme and navigation skeleton 2023-01-10 18:10:24 +08:00
tarsin 900c346ee8 Fixup! Convert login to compose, step 1, skeleton 2023-01-10 17:57:04 +08:00
tarsin 7541a87b21 Convert login to compose, step 1, skeleton 2023-01-10 17:54:55 +08:00
tarsin 2547c67ebd Fixup migrate to androidx navigation 2023-01-09 19:05:37 +08:00
tarsin 5ae6a0b287 Fixup migrate to androidx navigation 2023-01-09 19:00:04 +08:00
tarsin f718fc0c89 Reduce one more layout depth 2023-01-09 18:07:05 +08:00
tarsin 57e69ba7be Layout tweaks 2023-01-09 17:47:44 +08:00
tarsin 6168b3e047 Fixup! Use viewLifecycleOwner 2023-01-09 14:17:49 +08:00
tarsin 3e500500e0 Rewrite SelectSiteScene 2023-01-09 13:41:45 +08:00
tarsin 42f06ff84a Rename .java to .kt 2023-01-09 13:41:45 +08:00
tarsin a7824fc473 Convert WebViewSignInScene to kotlin 2023-01-09 13:35:53 +08:00
tarsin 67e7fd6928 Rename .java to .kt 2023-01-09 13:35:53 +08:00
tarsin 450e8badba GetProfile when signin with cookies 2023-01-09 13:21:48 +08:00
tarsin 704c211e11 Rewrite CookieSignInScene with modern viewBinding 2023-01-09 13:14:30 +08:00
tarsin 0ad871cb5b Rename .java to .kt 2023-01-09 13:14:30 +08:00
tarsin 9920605570 Checkout xz to 5.4.0 2023-01-09 12:56:10 +08:00
tarsin b1e8678dbf Fixup! Rewrite SignInScene with modern coroutines 2023-01-08 22:30:35 +08:00
tarsin e2cd2ddc0d Rewrite SignInScene with modern coroutines 2023-01-08 22:22:34 +08:00
tarsin a3be0dfddf Rewrite SignInScene 2023-01-08 21:33:50 +08:00
tarsin 4b615ffc0b Rename .java to .kt 2023-01-08 21:33:50 +08:00
tarsin 64cbd189ad Convert to kotlin 2023-01-08 21:15:26 +08:00
tarsin 0fae5a128f Rename .java to .kt 2023-01-08 21:15:26 +08:00
tarsin 518b44216f Navchecked item now handled internally 2023-01-08 20:57:59 +08:00
tarsin 9ef4512ef9 Reformat code 2023-01-08 20:42:23 +08:00
tarsin 8bb5f1e955 Initial migrate to androidx navigation (#647)
* Neutralize scene firstly

* Remove StageLayout

* Add nav graph

* Rename .java to .kt

* Convert BaseScene to kotlin

* Add GalleryDetailScene to navgraph

No need to check topfragment is callback target fragment, since these callback must be cancelled in fragment's lifecycle when DestroyView

* Restore funcs

* Restore preview

* Revert "Convert BaseScene to kotlin"

Should not do it here
This reverts commit 5d2e3af6ad85fef7604d8df19ee58a957390cdb5.

* Revert "Rename .java to .kt"

This reverts commit 128c655bc98fc54528d58534651d61ff47b2284d.

* Restore drawer

* Remove all getScene usage

* Remove all getStageActivity usage

* Remove all getStageActivity usage

* setupWithNavController

* Refactor MainActivity with viewBinding

* Add navdrawer corresponding navdestination

* Migrate All normal fragment

* Add anims

* Fix statusbar flicker

* Remove SceneApplication

* Remove SceneFragment

* Remove StageActivity

* Drop scene totally

* Dynamically set navgraph

* Restore signin

* Restore login via cookies

* Restore login via webview

* Add some TODOs

* Fixup

* Restore onProvideAssistContent

* Restore checkClipboard

* Restore intent handle

* Fix handle intent

* Remove unused EhUrlOpener

* Let system handle that intent

* Restore handle download intent

* Restore AboveSnackerLayout

* Fix the last navigation
2023-01-08 20:37:11 +08:00
FooIbar 8f8932f617 Fix potential integer overflow 2023-01-08 18:06:49 +08:00
tarsin 23fd1d75c4 Fix supertype corresponding parameter warning 2023-01-08 09:15:22 +08:00
stevenyomi 593dcef550 Fix floating-point error in navigate pan (#8856) 2023-01-08 09:04:53 +08:00
o0kam1 d769fe9074 fix #642 2023-01-07 22:13:16 +08:00
tarsin 77a8fec790 Bump Version
Releases / Build (push) Has been cancelled
2023-01-07 09:10:50 +08:00
renovate[bot] 34440edfaa Update dependency com.google.android.material:material to v1.8.0-rc01 2023-01-06 21:54:07 +00:00
tarsin 3c3d8784ad Fixup cleanup Mainthread db query 2023-01-06 18:48:06 +08:00
tarsin 1591abc92c It's impossible no activity receive that Intent, remove workaround 2023-01-06 18:38:38 +08:00
tarsin 0ffa0c34f0 Remove some NotifyDataSetChanged usage 2023-01-06 18:27:54 +08:00
tarsin c7dc94b61b Singletonize GalleryListScene OnBackPressedCallback 2023-01-06 18:11:41 +08:00
tarsin a8b83871a4 Convert GalleryListScene drawer to ViewBinding 2023-01-06 18:07:57 +08:00
tarsin 6f547984ff Convert GalleryListScene to ViewBinding 2023-01-06 17:59:15 +08:00
tarsin 3979c11317 Refine GalleryListScene nullability 2023-01-06 17:36:55 +08:00
tarsin 2a1ec985f4 GalleryListScene urlBuilder cleanup 2023-01-06 17:27:30 +08:00
tarsin 2a1d49e945 Fix DownloadsScene delete label, #629 2023-01-06 13:02:12 +08:00
FooIbar 7b7e6140d2 Improve domain fronting description 2023-01-06 12:56:25 +08:00
tarsin 907cfd9694 Fix DownloadsScene add label, #629 2023-01-06 12:42:13 +08:00
tarsin b51b1d1ec6 Convert DownloadsScene to kotlin, #629 2023-01-06 12:37:24 +08:00
tarsin 6a026300e4 Rename .java to .kt 2023-01-06 12:37:24 +08:00
tarsin 8511907390 Fix HistoryScene history clearall, #629 2023-01-06 12:22:57 +08:00
tarsin 05a5be336e Fix HistoryScene history delete, #629 2023-01-06 12:21:18 +08:00
tarsin 5434b1ce19 Fix GalleryListScene quicksearch delete, #629 2023-01-06 12:19:20 +08:00
tarsin c2d4a4367c Fix GalleryListScene quicksearch insert, #629 2023-01-06 12:16:37 +08:00
tarsin de2f277513 Convert GalleryListScene to kotlin, #629 2023-01-06 12:11:20 +08:00
tarsin 283e522037 Rename .java to .kt 2023-01-06 12:11:20 +08:00
tarsin caebe4c23b Initialize database earlier, #629 2023-01-06 11:54:26 +08:00
tarsin b195277c9a Minor Refactor 2023-01-06 11:32:53 +08:00
tarsin 6b99cb6253 Make GalleryDetail well typed and implement Parcelable 2023-01-06 11:07:45 +08:00
txyyh a822bb670a Change bug-report issue template 2023-01-06 11:05:24 +08:00
tarsin af7cc11647 Extract properties to a standalone AbstractGalleryInfo 2023-01-06 11:04:18 +08:00
tarsin 0afe331d3d Rename GalleryInfoImpl to BaseGalleryInfo 2023-01-06 11:02:44 +08:00
tarsin 7af40f0cd7 Revert "Not good solution :("
This reverts commit 3283851dc4.
2023-01-06 11:00:28 +08:00
tarsin 52dc699ee2 Revert "Cleanup primary constructors"
This reverts commit ec28e21897.
2023-01-06 11:00:17 +08:00
tarsin ec28e21897 Cleanup primary constructors 2023-01-06 10:41:33 +08:00
tarsin 3283851dc4 Not good solution :( 2023-01-06 10:30:31 +08:00
tarsin ec5e0c7b25 Fuck you Jetbrains! 2023-01-06 09:55:09 +08:00
tarsin 13660cb70a Cleanup JvmFields in GalleryInfo 2023-01-06 09:33:24 +08:00
tarsin ce008f5576 Bump Version
Releases / Build (push) Has been cancelled
2023-01-06 08:49:47 +08:00
tarsin 25f7e8b35c Always show toast in BottomSheet mode, Resolve #633 2023-01-06 08:47:37 +08:00
tarsin 109398d955 Fix enter Reader from previews 2023-01-06 08:38:57 +08:00
tarsin 4a8551087b Fix crash when open history 2023-01-06 08:15:42 +08:00
tarsin 34febe3545 Fix crash when open download 2023-01-06 08:09:11 +08:00
tarsin 53a0307a8e Bump Versions
Releases / Build (push) Has been cancelled
2023-01-05 21:42:34 +08:00
tarsin 1c6f624fd1 Kotlinize EhExceptions 2023-01-05 21:21:47 +08:00
tarsin 780e5f08f9 Rename .java to .kt 2023-01-05 21:21:47 +08:00
tarsin 1bb73b1147 Kotlinize 2023-01-05 21:15:57 +08:00
tarsin 09b0d4d6c4 Rename .java to .kt 2023-01-05 21:15:57 +08:00
tarsin 7e3856fa05 Refine dao nullability 2023-01-05 21:10:14 +08:00
tarsin fbcb3f5436 Get download manager earlier, #629 2023-01-05 19:44:56 +08:00
tarsin 324b4ced06 Convert DAO to kotlin, step 3, and use ksp 2023-01-05 19:15:19 +08:00
tarsin 79c2abe790 Rename .java to .kt 2023-01-05 19:15:19 +08:00
tarsin cdab9168fc Convert DAO to kotlin, step 2 2023-01-05 19:01:35 +08:00
tarsin ce272724de Rename .java to .kt 2023-01-05 19:01:35 +08:00
tarsin 2fa0a19d3b Convert DAO to kotlin, step 1 2023-01-05 18:45:45 +08:00
tarsin 38b9a6e04a Rename .java to .kt 2023-01-05 18:45:45 +08:00
tarsin 22d3c2b6bf Migrate to Parcelize step 2 2023-01-05 17:42:07 +08:00
tarsin 218a8397c4 Rename .java to .kt 2023-01-05 17:42:07 +08:00
tarsin fd990da71e Fixup! Execute request in fragment's lifecycle coroutine scope, Resolve #628 2023-01-05 15:52:23 +08:00
小白-白 4a70c89a4d Restore download dir to avoid redownload
Resolve #547
2023-01-05 15:19:01 +08:00
小白-白 7e3143678f Drop clear download path cache
Useless and will cause redownload
2023-01-05 15:19:01 +08:00
renovate[bot] d80e09dfba Update plugin com.android.application to v8.0.0-alpha11 2023-01-04 23:13:36 +00:00
tarsin 3b3b251c21 Reformat code 2023-01-04 17:04:12 +08:00
tarsin 1e848bd70f Refine request api 2023-01-04 16:52:47 +08:00
tarsin fab2295445 Reorganize request code and fix bugs 2023-01-04 16:24:59 +08:00
tarsin a913bccc9e Rename .java to .kt 2023-01-04 16:24:59 +08:00
tarsin 57e7f89814 Execute request in fragment's lifecycle coroutine scope 2023-01-04 15:02:16 +08:00
tarsin f98fd5e5f1 Handle Call cancellation internally 2023-01-04 14:31:40 +08:00
tarsin e6af2e5c6b Make EhEngine task well cancelled in coroutine 2023-01-04 13:43:51 +08:00
tarsin d219989ec7 Rename .java to .kt 2023-01-04 13:43:51 +08:00
小白-白 b9daf608ff Add more info for torrent download 2023-01-04 12:26:46 +08:00
tarsin 869da61fd0 Use coroutine CancellationException 2023-01-04 12:26:09 +08:00
tarsin 9f79e2505c Coroutinize EhClient (#622)
* Rename .java to .kt

* Coroutinize EhClient to improve performance and clarity

* Nullability Compatibility

* Refine control flow

* Fix nullability
2023-01-04 11:51:31 +08:00
tarsin 3000b36dbd Exclude unused dependency 2023-01-03 16:23:03 +08:00
tarsin 1d21b0e5c4 Use ktx dependency 2023-01-03 15:56:59 +08:00
tarsin 8d6b0926d6 Convert to kotlin-parcelize 2023-01-03 15:33:04 +08:00
tarsin 4c39d13c80 Rename .java to .kt 2023-01-03 15:33:04 +08:00
tarsin 327dd1c73f Minor Refactor 2023-01-03 12:29:39 +08:00
tarsin b36b8f3234 Rename .java to .kt 2023-01-03 12:29:39 +08:00
FooIbar 5f6da284fb fixup! Create download dir only in download mode 2023-01-03 11:12:28 +08:00
tarsin 83d27e18cb Migrate to kotlin-parcelize 2023-01-03 09:56:37 +08:00
FooIbar e291bd58a8 Check if ehTags is initialized 2023-01-03 08:56:30 +08:00
FooIbar 23acdb6492 Support non-H@H archive download 2023-01-03 08:53:25 +08:00
FooIbar 3b5e56b049 Create download dir only in download mode
Do not try to find download dir, should use restore download instead
2023-01-02 22:10:27 +08:00
tarsin 57d4967f2a Implement #612 2023-01-02 21:39:46 +08:00
tarsin 80fcda149b Resolve #615 2023-01-02 21:03:47 +08:00
o0kam1 1a86eef52c add copy image 2023-01-02 20:45:24 +08:00
FooIbar 98d401f9c3 Fix callback order 2023-01-02 15:31:25 +08:00
FooIbar 0f2fae886e fixup! Set toolbar text when exiting search view 2023-01-02 15:31:25 +08:00
tarsin f30a9093af Remove databinding runtime
It is introduced when we migrated to tachiyomi reader
Seems tachiyomi's other parts uses databinding but not its reader
2023-01-02 13:26:03 +08:00
tarsin b4334da202 Fixup! Convert all kotlin fragment to viewbinding 2023-01-02 13:17:47 +08:00
tarsin 416fb3bda9 Fixup! Convert all kotlin fragment to databinding 2023-01-02 12:52:54 +08:00
tarsin 7e0fc2e364 Convert all kotlin fragment to databinding 2023-01-02 12:46:48 +08:00
tarsin 540d290be2 Convert HistoryScene to databinding 2023-01-02 11:32:55 +08:00
小白-白 53974ad88d Don't cancel task if mData is empty
Resolve #587
2023-01-02 11:18:58 +08:00
tarsin cd9b5e5a44 Simplify onNavigationClick 2023-01-02 10:56:20 +08:00
tarsin d9959bbcb2 Remove onBackPressed support on scene 2023-01-02 10:47:41 +08:00
tarsin d7dfd53244 Handle GalleryListScene SearchMode OnBackPressedCallback 2023-01-02 10:29:01 +08:00
tarsin 84489bf61b Handle GalleryCommentsScene EditPanel OnBackPressedCallback 2023-01-02 10:16:20 +08:00
tarsin 3fdb56390e Use colorAccent for Filled heart button 2023-01-02 09:44:22 +08:00
tarsin a34e766f70 Fix drawer OnBackPressedCallback 2023-01-02 09:35:53 +08:00
FooIbar d74023d4fa Resolve #605 2023-01-02 08:44:31 +08:00
FooIbar 41572e98ba Resolve #604 2023-01-02 08:44:31 +08:00
FooIbar 063087c87e Add horizontal clip spacing to avoid layout change 2023-01-02 08:44:31 +08:00
FooIbar 4a5d53d713 Set toolbar text when exiting search view 2023-01-02 08:44:31 +08:00
tarsin 9a262525ec Revert "Exit activity directly when back-pressed"
This reverts commit e327b4e84b.
2023-01-01 17:53:25 +08:00
tarsin e327b4e84b Exit activity directly when back-pressed 2023-01-01 17:26:39 +08:00
tarsin c128a2b518 Fix preference flow being cancelled 2023-01-01 17:12:05 +08:00
tarsin a3263ab4af Avoid unnecessary coroutine context switch 2023-01-01 16:58:34 +08:00
tarsin 4a4efe4725 Convert All activity to kotlin 2023-01-01 16:11:28 +08:00
tarsin 89d2433369 Rename .java to .kt 2023-01-01 16:11:28 +08:00
tarsin 023175ce6d Fixup convert to kotlin 2023-01-01 15:34:06 +08:00
FooIbar 21c8d24d57 Add size and cost for archive download 2023-01-01 15:27:22 +08:00
tarsin e49f5c95f4 Handle fablayout expand backpress on its callback 2023-01-01 15:26:32 +08:00
tarsin 6912e578ca Convert fabLayout to kotlin and refine nullability 2023-01-01 14:36:17 +08:00
tarsin 8680cff2b2 Rename .java to .kt 2023-01-01 14:36:17 +08:00
tarsin aea75d44e3 Happy New Year 2023! 2023-01-01 08:38:50 +08:00
tarsin 80481f1778 Update copyleft year 2023-01-01 08:37:32 +08:00
FooIbar 3ea7ff8202 Resolve #598 2022-12-31 19:37:33 +08:00
小白-白 b5979c0528 Fix comments covered by fab 2022-12-31 16:47:02 +08:00
小白-白 f0f79a52b8 Support filter comments
Resolve #516
2022-12-31 16:47:02 +08:00
小白-白 b549acc1de Add page number limit 2022-12-31 16:47:02 +08:00
小白-白 2505cc5d00 Don't show progress bar on last page 2022-12-31 16:47:02 +08:00
小白-白 d5af66b3fc Fix search bar set selection 2022-12-31 16:47:02 +08:00
小白-白 f680dc7e46 Fix copy trans 2022-12-31 16:47:02 +08:00
tarsin ffd8bbb1ce Refine missing comments 2022-12-31 12:38:50 +08:00
tarsin 216a043094 Set bar style correctly 2022-12-31 12:35:20 +08:00
tarsin 021eb5f828 Fix 3-button navigation 2022-12-31 12:27:47 +08:00
tarsin f3bf8849e9 Cleanup unused res 2022-12-31 11:48:58 +08:00
tarsin 128f7100d7 Minor cleanup 2022-12-31 11:19:34 +08:00
FooIbar 2cf53dd00f Disable landscape zoom by default 2022-12-30 17:27:01 +08:00
tarsin 4a674e46e6 Fixup! Handle drawer on back-pressed 2022-12-30 16:40:46 +08:00
tarsin f6b0134fdb Remove redundant comment 2022-12-30 14:33:20 +08:00
tarsin 675a6b863e Handle searchview backpress in separated callback 2022-12-30 13:43:39 +08:00
tarsin b570eeef93 Don't allow open drawer in search mode 2022-12-30 13:12:45 +08:00
tarsin 71a8b4e607 Handle drawer on back-pressed 2022-12-30 12:35:19 +08:00
renovate[bot] 89b866b0fc Update dependency gradle to v8.0-rc-1 2022-12-29 18:34:17 +00:00
tarsin bcce20d9ac Remove unnecessary okhttpclient rebuild 2022-12-29 21:18:47 +08:00
tarsin 420e760fb3 fixup! Convert spiderInfo cache to Coil DiskCache 2022-12-29 20:38:48 +08:00
FooIbar a168c95d79 fixup! Refactor getPageData 2022-12-29 20:28:16 +08:00
tarsin d8ccec45f9 Convert spiderInfo cache to Coil DiskCache 2022-12-29 20:27:33 +08:00
renovate[bot] 1316875602 Update plugin com.google.devtools.ksp to v1.8.0-1.0.8 2022-12-29 09:05:19 +08:00
FooIbar a85387c7c8 Add edit button for download label 2022-12-28 22:20:53 +08:00
renovate[bot] d357e83d06 Update plugin org.jetbrains.kotlin.android to v1.8.0 2022-12-28 14:17:07 +00:00
FooIbar 51546f6fea Add option for saving progress in quick search 2022-12-28 16:32:05 +08:00
tarsin b4aa5ec593 Revert "Get download dir in download mode only"
This reverts commit 241d9daae9.
2022-12-28 12:20:49 +08:00
tarsin 909380f592 Remove unused dirloader 2022-12-28 12:09:13 +08:00
tarsin d5f26643bf Support wide-color-gamut as config 2022-12-28 11:34:17 +08:00
tarsin a15dc4afdf Minor cleanup image decode codes 2022-12-28 09:57:50 +08:00
FooIbar b317bc334e Store GID in quick search name
Resolve #447
2022-12-27 15:44:02 +08:00
FooIbar d0a8d08b26 fixup! Support jump to last page 2022-12-27 15:43:01 +08:00
FooIbar f73a922a8e Cancel current task before saving instance state
Resolve #210
2022-12-26 20:33:19 +08:00
tarsin c6682ce718 Fix SwipeRefreshLayout theme 2022-12-26 19:01:35 +08:00
tarsin 764d1137ae Fixup sync with tachiyomi res 2022-12-26 17:29:02 +08:00
tarsin 02fc972a79 Remove confusing hint 2022-12-26 17:11:55 +08:00
tarsin 7fc727cf2e Resolve #579 2022-12-26 17:05:17 +08:00
FooIbar d77e1e6820 Remove redundant context parameter 2022-12-26 16:38:22 +08:00
FooIbar 241d9daae9 Get download dir in download mode only
Resolve? #381
2022-12-26 16:38:22 +08:00
FooIbar 462e0257a9 Save images to download location by default 2022-12-26 16:37:38 +08:00
renovate[bot] 66fbc7e821 Update dependency com.squareup.okhttp3:okhttp-bom to v5.0.0-alpha.11 2022-12-24 22:23:31 +00:00
FooIbar 3c37f22027 Use getBindingAdapterPosition() to get item position
Better solution for #201
2022-12-24 16:52:37 +08:00
tarsin 0d24bc6602 Thou shalt not neutralize nullability check 2022-12-24 12:20:55 +08:00
tarsin 5a50df8179 Upgrade dependencies 2022-12-24 11:53:58 +08:00
tarsin f1251fd150 Remove gradle properties enabled by default 2022-12-24 11:52:05 +08:00
tarsin 16202efc7b Allow obfuscate settings 2022-12-24 10:35:37 +08:00
tarsin 26c6052286 Rename .java to .kt 2022-12-24 10:35:37 +08:00
tarsin 7ce386a7b8 Cleanup OpenGL usage
Our bitmap/hwui may use vulkan ctx
2022-12-24 09:42:16 +08:00
tarsin e43cd0a039 Minor refactor 2022-12-23 19:12:39 +08:00
tarsin 70be405032 Free native source in advance if image is not Animated 2022-12-23 18:58:03 +08:00
tarsin 9f3ad8142a Remove largeHeap flag
Now that pixels are stored in GraphicBuffer, we no longer need large dalvik or native heap
2022-12-23 18:52:48 +08:00
FooIbar 51d9ece30c Init readingModeSettings before showing
Resolve #539
2022-12-22 21:45:07 +08:00
FooIbar d8e79f879f Fix missing space in copyright 2022-12-22 21:45:07 +08:00
FooIbar 76d50b56ee Show progress on goTo 2022-12-22 13:37:34 +08:00
renovate[bot] c8d1e957a1 Update plugin com.android.application to v8.0.0-alpha10 2022-12-22 03:14:33 +00:00
FooIbar b287af4824 Refactor getPageData 2022-12-21 18:28:52 +08:00
FooIbar 416ef8d6d7 fixup! Remove unused fab 2022-12-21 18:28:52 +08:00
renovate[bot] 7c71abb985 Update plugin com.google.devtools.ksp to v1.8.0-RC2-1.0.8 2022-12-21 11:37:05 +08:00
renovate[bot] 900f2462a7 Update plugin org.jetbrains.kotlin.android to v1.8.0-RC2 2022-12-20 18:56:50 +00:00
txyyh dd982c6cb9 Release 1.8.5.1 2022-12-20 14:01:10 +00:00
tarsin 927a011c9d Upload mapping for release as well 2022-12-20 21:58:50 +08:00
tarsin ad0ad47a40 Support RTL Slider
But Idk whether we really support RTL :(
2022-12-20 21:30:20 +08:00
tarsin 71a424c15d Remove unused fab 2022-12-20 21:20:42 +08:00
FooIbar 145b7049a8 Support jump to last page
Resolve #466
2022-12-20 21:03:43 +08:00
tarsin 323d258bc2 Fix RTL load 2022-12-20 20:57:45 +08:00
tarsin 56ba52d7b9 Do not recycle bitmap in pager, add back lrucache
TODO: use coil instead
2022-12-20 20:43:15 +08:00
tarsin 54bd6f6ed8 Fix memleak for archive 2022-12-20 19:58:40 +08:00
tarsin f06167ba4b Sync with tachiyomi, support load progress 2022-12-20 19:03:57 +08:00
renovate[bot] 60438ae1ed Update dependency gradle to v8.0-milestone-6 (#562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-20 17:23:02 +08:00
FooIbar 1b87750b87 fixup! Fix isTranslatable (#555)
* Use appcompat to auto store locales

* fixup! Fix isTranslatable #553

* Code cleanup
2022-12-20 08:36:55 +08:00
FooIbar 40cdc566c8 Upload mapping files in ci 2022-12-20 08:33:56 +08:00
txyyh 428ae07e7f Release 1.8.5.0 2022-12-19 14:59:35 +00:00
tarsin 481ad22b53 Code cleanup 2022-12-19 13:00:06 +08:00
FooIbar 65f58b93ed Update mCurrentIndex on page move
Resolve #538
2022-12-19 12:09:31 +08:00
FooIbar 45411fa14d Use notifyAll() instead of notify() on mDecodeRequestQueue
Resolve #549
2022-12-19 08:13:31 +08:00
tarsin 8ba1ba4fb1 fixup! Minor Refactor 2022-12-18 14:06:55 +08:00
tarsin 8427e1b3a5 fixup! Minor Refactor 2022-12-18 13:55:16 +08:00
tarsin 32f0f2a0cf Fix isTranslatable 2022-12-18 13:02:12 +08:00
tarsin 8b94997cde Minor Refactor 2022-12-18 12:52:30 +08:00
tarsin bd2d54f774 fixup! Rewrite search suggestion with kotlin flow, remove suggestion limit 2022-12-18 11:31:49 +08:00
FooIbar 34be285510 Do not add neighbors on page success
Might resolve #536 #540
2022-12-18 09:13:38 +08:00
FooIbar 7b906f3c3e Update tag database format 2022-12-18 07:55:37 +08:00
tarsin 82ba22cda2 Resolve #542 2022-12-17 14:24:24 +08:00
tarsin b1a9601ae9 fixup! Rewrite search suggestion with kotlin flow, remove suggestion limit 2022-12-17 13:08:08 +08:00
tarsin 7c1a297a99 Remove unused shared libs 2022-12-17 12:59:02 +08:00
FooIbar a8195c0e7c Map archive index with filename order
Try to resolve #427
2022-12-17 12:53:13 +08:00
FooIbar 08713e3922 Use SummaryProvider instead of String formatting marker 2022-12-17 12:53:13 +08:00
tarsin 0485d52e3e Rewrite search suggestion with kotlin flow, remove suggestion limit 2022-12-17 12:47:37 +08:00
tarsin 8b1723e19b Rename .java to .kt 2022-12-17 12:47:37 +08:00
tarsin 9cb683dc55 Fix crash when zoomed and try to swipe to next page 2022-12-16 22:07:09 +08:00
tarsin dce1589c19 Fix remaining request not cancelled for detached image 2022-12-16 21:35:28 +08:00
FooIbar 107cf16d36 Remove unused maven repo due to a677d95 2022-12-16 03:50:35 +08:00
FooIbar 2bab97c829 Better way to handle tag translation flag 2022-12-16 03:50:35 +08:00
tarsin 5242d825c2 Change location 2022-12-15 11:43:42 +08:00
FooIbar 11d3799b03 Change tag database format to json for better maintainability 2022-12-15 11:38:32 +08:00
FooIbar 55c7f6b896 Support tag suggestion for all languages 2022-12-15 06:43:50 +08:00
tarsin a09d607177 Do colorspace trans if needed 2022-12-14 17:24:19 +08:00
txyyh 626a101208 Rename Repository 2022-12-14 07:39:04 +00:00
tarsin f4ca3acbf6 Fix statusbar 2022-12-14 14:32:40 +08:00
tarsin 86835a3bd7 Remove toolbar and fix theme 2022-12-14 14:26:15 +08:00
tarsin 0ef46e2e11 Move image blobs to another repo 2022-12-14 12:24:17 +08:00
tarsin 04878447b7 Fixup translation import 2022-12-14 12:07:35 +08:00
tarsin 3669d5d90e Resolve #527 2022-12-14 11:25:06 +08:00
tarsin ee5e222a13 Resolve #525 2022-12-14 11:11:20 +08:00
tarsin 9ea44ff59a Remove blur when adjust color filter 2022-12-14 10:58:32 +08:00
tarsin 0474e2c0f5 Kill read settings in preference, clean up res 2022-12-14 10:39:06 +08:00
tarsin 4a909d4b93 Support readWithVolumeKeys 2022-12-14 10:32:46 +08:00
txyyh 8dbe24ab55 Release 1.8.5.0-preview 2022-12-13 14:34:43 +00:00
tarsin 25f7b005cb Mark reader support wideColorGamut 2022-12-13 21:36:26 +08:00
tarsin 23f55ea208 Update gradle wrapper 2022-12-13 21:31:43 +08:00
tarsin 5f19fa0f1f Restore config 2022-12-13 21:18:11 +08:00
tarsin 58fbdcfcdd Sync tachiyomi reader translations 2022-12-13 21:02:54 +08:00
tarsin e33cca9fd9 Reformat code 2022-12-13 20:54:45 +08:00
tarsin b25e3d5454 Remove unused res 2022-12-13 20:51:01 +08:00
tarsin 583f272daa Remove unsupported setting 2022-12-13 20:46:22 +08:00
tarsin 2a83ad63be Inline DirectionalViewPager 2022-12-13 20:29:44 +08:00
tarsin 6fdc908ad1 Support save cursor 2022-12-13 18:48:48 +08:00
tarsin 5213caac49 Drop glgallery (#519)
* Neutralize GlGallery

* Enable dataBinding and viewBinding

* Add tachiyomi reader layout

* Cleanup Dead-code

* Rename .java to .kt

* Fix activity rename

* Nullability changes

* Add basic tachiyomi reader imageviews

* Fix size increase

* Let's cleanup later

* Add some tachiyomi webtoon widgets

* Import tachiyomi webtoon pager without chapter transition

* Remove rx dependency

* Rewrite GalleryProvider interface with flow

* groundwork for image load

* Refine webtoon pageholder

* Force us hw bitmap, refine recycle

* Strip tachiyomi decoder usage

* Keep our own SubsamplingScaleImageView, strip tachiyomi decoder to avoid apk bloated

* Import other PageViewers

* Add webtoon viewer config

* groundFix ReaderActivity

* Add tachiyomi Preferences

* Readpreference

* Fix fullscreen

* Move related class to tachiyomi package

* Fix pageload

* Add readerconfig

* Add ViewerConfig hook

* Add Reader bottomsheet settings

* Fix reader setting apply

* Refine

* Fix save share image

* Refine theme
2022-12-13 15:51:27 +08:00
FooIbar ca0518c747 Verify cookies through getProfile (#523)
Verify cookies through getProfile, Resolve? #283
2022-12-13 09:00:56 +08:00
FooIbar 298cd29537 Disable proxy when domain fronting is enabled (#522) 2022-12-13 08:26:53 +08:00
FooIbar 1a963a3239 Notify next items changed after items inserted (#520)
* Notify next items changed after items inserted, Resolve #201

* Code cleanup
2022-12-12 19:09:05 +08:00
FooIbar 683c8dc5c5 Fix onApplySearch getting called twice 2022-12-12 08:42:21 +08:00
FooIbar 2087970de6 Use MODE for favoriteSlot in HistoryInfo 2022-12-12 01:03:09 +08:00
FooIbar 5bd270fe6a Parse favoriteSlot for GalleryDetail 2022-12-12 01:03:09 +08:00
FooIbar 0992fcfcb0 Fix local favorites search 2022-12-11 20:11:30 +08:00
tarsin fc12395f7e Limit coil threads 2022-12-11 17:21:02 +08:00
tarsin 57b291a4e8 Checkout xz-utils to 5.2.9 2022-12-11 16:54:17 +08:00
tarsin 3adf391478 Checkout libarchive to 3.8.2 2022-12-11 16:49:57 +08:00
tarsin a859941845 Reformat Code 2022-12-11 12:26:22 +08:00
tarsin 8d1fc85638 Fix filter chip 2022-12-11 12:22:31 +08:00
tarsin 51ece1c472 Adjust block ips 2022-12-11 11:59:33 +08:00
tarsin 45f67ec37f Fix webtoon down sampling 2022-12-11 11:57:39 +08:00
FooIbar 85b559c989 Generate simpleLanguage for local favorites 2022-12-11 11:50:26 +08:00
FooIbar 1382050b5f Request UConfig on login 2022-12-11 11:50:26 +08:00
FooIbar fb970752fd Remove unused parameter 2022-12-11 11:50:26 +08:00
FooIbar 3d256a402f Auto close response to avoid resource leak 2022-12-11 11:50:26 +08:00
tarsin a677d95d34 Use mca 1.8.0beta01 2022-12-11 08:11:16 +08:00
FooIbar 452c0f2846 Use compose BOM instead 2022-12-10 07:36:24 +08:00
txyyh e568e688f5 Release 1.8.4.0-alpha02 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 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 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 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 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
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 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 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
662 changed files with 34486 additions and 57124 deletions

No files matched your search

+30 -15
View File
@@ -1,19 +1,30 @@
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:
label: 复现步骤 / Step to reproduce
description: |
在此处写下复现的方式
Describe how to reproduce here
请从全新安装应用开始,在此处写下复现的方式。请尽量附带录屏
Describe how to reproduce here with fresh installation, with screen recording if possible.
placeholder: |
0. 重新安装应用 / Fresh install
1.
2.
3.
[录屏] / [Screen recording]
validations:
required: true
@@ -44,12 +55,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 +81,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
@@ -89,15 +113,6 @@ body:
validations:
required: true
- type: textarea
id: log
attributes:
label: 应用日志 / App logs
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
placeholder: 06-15 17:44:53.704 23382 23382 E ...
validations:
required: true
- type: checkboxes
id: check
attributes:
@@ -119,4 +134,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
+21 -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
@@ -52,3 +56,10 @@ jobs:
with:
name: x86-${{ github.sha }}
path: app/build/outputs/apk/release/app-x86-release.apk
- name: Update mapping
uses: actions/upload-artifact@v3
with:
name: mapping-${{ github.sha }}
path: app/build/outputs/mapping/release/mapping.txt
retention-days: 5
+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
+42 -14
View File
@@ -12,33 +12,61 @@ 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
app/build/outputs/mapping/release/mapping.txt
- 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
+48 -21
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="https://github.com/Ehviewer-Overhauled/Art/blob/master/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,44 +33,59 @@
</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="https://github.com/Ehviewer-Overhauled/Art/blob/master/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="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-fdroid.svg" width="200px"/></a>
# Screenshot
![screenshot-01](art/screenshot-01.png)
![screenshot-02](art/screenshot-02.png)
![screenshot-01](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-01.png)
![screenshot-02](https://github.com/Ehviewer-Overhauled/Art/blob/master/screenshot-02.png)
# 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/)
- [greenDAO](https://github.com/greenrobot/greenDAO)
- [jsoup](https://github.com/jhy/jsoup)
- [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/)
@@ -67,7 +94,7 @@ Here is the libraries
Copyright 2014-2019 Hippo Seven
Copyright 2020-2022 NekoInverter
Copyright 2022 Tarsin Norbin
Copyright 2022-2023 Tarsin Norbin
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.
+2 -1
View File
@@ -4,4 +4,5 @@ manifest-merger-release-report.txt
/src/main/libs
/src/main/obj
# Intellij
*.iml
*.iml
/.cxx
+77 -74
View File
@@ -1,23 +1,21 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'kotlin-android'
id 'kotlin-parcelize'
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 +30,30 @@ android {
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 31
minSdkVersion 28
targetSdkVersion 33
versionCode 180003
versionName "1.8.0.3"
versionCode 180030
versionName "1.8.5.3-rc01"
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"
]
}
lint {
@@ -56,7 +64,8 @@ android {
packagingOptions {
resources {
excludes += "/META-INF/**"
excludes += "/META-INF/CHANGES"
excludes += "/META-INF/README.md"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
@@ -78,89 +87,83 @@ android {
}
}
sourceSets {
main {
java.srcDirs += 'src/main/java-gen'
}
buildFeatures {
buildConfig true
compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.0"
}
namespace 'com.hippo.ehviewer'
}
tasks.withType(JavaCompile) {
task -> task.dependsOn ":daogenerator:executeDaoGenerator"
}
dependencies {
def activity_version = "1.6.0-alpha05"
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"
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"
// https://developer.android.com/jetpack/androidx/releases/activity
implementation "androidx.activity:activity-compose:1.6.1"
implementation "androidx.appcompat:appcompat:1.6.0"
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
implementation "androidx.browser:browser:1.4.0"
def collection_version = "1.3.0-alpha02"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
// https://developer.android.com/jetpack/androidx/releases/compose-material3
implementation platform("androidx.compose:compose-bom:2023.01.00")
implementation "androidx.compose.material:material-icons-extended"
implementation "androidx.compose.material3:material3"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha05"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
// https://developer.android.com/jetpack/androidx/releases/lifecycle
implementation "androidx.lifecycle:lifecycle-process:2.5.1"
def core_version = "1.9.0-alpha05"
implementation "androidx.core:core:$core_version"
implementation "androidx.core:core-ktx:$core_version"
// https://developer.android.com/jetpack/androidx/releases/navigation
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-compose:$nav_version"
implementation "androidx.customview:customview:1.2.0-alpha01"
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
// https://developer.android.com/jetpack/androidx/releases/paging
implementation "androidx.paging:paging-runtime-ktx:3.2.0-alpha03"
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-rc01'
def fragment_version = "1.5.1"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
// https://developer.android.com/jetpack/androidx/releases/room
def room_version = "2.5.0"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
def lifecycle_version = "2.6.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata:$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"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
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"
implementation "androidx.vectordrawable:vectordrawable:1.2.0-beta01"
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.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Dead Dependency
implementation 'com.github.tachiyomiorg:DirectionalViewPager:1.0.0' // Dead Dependency
// https://github.com/google/accompanist/releases
implementation "com.google.accompanist:accompanist-themeadapter-material3:0.28.0"
implementation 'com.google.android.material:material:1.8.0-rc01'
// https://square.github.io/okhttp/changelogs/changelog/
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-coroutines'
implementation 'com.squareup.okio:okio-jvm:3.3.0'
implementation 'dev.chrisbanes.insetter:insetter:0.6.1' // Dead Dependency
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
// https://coil-kt.github.io/coil/changelog/
// implementation 'io.coil-kt:coil:2.2.2'
implementation 'com.github.Ehviewer-Overhauled.coil:coil:d65881d'
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: 'org.jetbrains.kotlin', module: 'kotlin-android-extensions-runtime'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}
+5 -23
View File
@@ -2,33 +2,15 @@
public static final ** CREATOR;
}
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void check*(...);
public static void throw*(...);
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-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 { }
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
-keep class com.hippo.ehviewer.dao.* { *; }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
-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
+43 -45
View File
@@ -1,20 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.software.leanback"
@@ -23,48 +9,50 @@
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-feature
android:glEsVersion="0x00020000"
android:required="true" />
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:name="com.hippo.ehviewer.EhApplication"
android:name=".EhApplication"
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">
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="tiramisu">
<activity
android:name="com.hippo.ehviewer.ui.MainActivity"
android:name=".ui.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
android:banner="@mipmap/ic_leanback_banner"
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
@@ -85,26 +73,26 @@
<data android:host="g.e-hentai.org" />
<data android:host="lofi.e-hentai.org" />
</intent-filter>
<intent-filter android:label="@string/keyword_search">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter android:label="@string/image_search">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name="com.hippo.ehviewer.ui.GalleryActivity"
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
android:configChanges="screenSize"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -138,22 +126,33 @@
<data android:mimeType="application/x-xz-compressed-tar" />
</intent-filter>
</activity>
<activity
android:name="com.hippo.ehviewer.ui.SettingsActivity"
android:name=".ui.SettingsActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
android:name=".ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name=".shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<service
android:name="com.hippo.ehviewer.download.DownloadService"
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<service
android:name=".download.DownloadService"
android:foregroundServiceType="dataSync"
android:label="@string/download_service_label" />
@@ -162,11 +161,10 @@
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
</manifest>
@@ -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 fdutils.c gifutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
+419
View File
@@ -0,0 +1,419 @@
/*
* 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;
typedef struct {
const char *filename;
int index;
} entry;
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;
static entry *entries = NULL;
static size_t entryCount = 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 inline int compare_entries(const void *a, const void *b) {
const char *fa = ((entry *) a)->filename;
const char *fb = ((entry *) b)->filename;
int a_len = (int) strlen(fa);
int b_len = (int) strlen(fb);
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
}
static long archive_map_entries_index(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
const char *name = archive_entry_pathname(ctx->entry);
if (filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
count++;
}
}
if (!count)
LOGE("%s", archive_error_string(ctx->arc));
qsort(entries, entryCount, sizeof(entry), compare_entries);
return count;
}
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");
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
entryCount = 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);
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 {
entries = calloc(entryCount, sizeof(entry));
r = archive_map_entries_index(ctx);
}
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);
index = entries[index].index;
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);
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
free(entries);
}
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);
index = entries[index].index;
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);
index = entries[index].index;
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) {
EH_UNUSED(thiz);
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 */
+43
View File
@@ -0,0 +1,43 @@
#include <jni.h>
#include <sys/mman.h>
#include <android/log.h>
#include <string.h>
#include <errno.h>
#define LOG_TAG "mmap_utils"
#include "ehviewer.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;
}
JNIEXPORT jobject JNICALL
Java_com_hippo_Native_mapFd(JNIEnv *env, jclass clazz, jint fd, jlong capability) {
void *addr = mmap(0, capability, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
if (addr != MAP_FAILED) {
return (*env)->NewDirectByteBuffer(env, addr, capability);
} else {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return NULL;
}
}
JNIEXPORT void JNICALL
Java_com_hippo_Native_unmapDirectByteBuffer(JNIEnv *env, jclass clazz, jobject buffer) {
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (munmap(addr, size)) LOGE("munmap addr:%p, size%zu failed!", addr, size);
}
+44
View File
@@ -0,0 +1,44 @@
#include <jni.h>
#include <string.h>
#include <stdbool.h>
#include <android/log.h>
#define LOG_TAG "gifUtils"
#include "ehviewer.h"
#define GIF_HEADER_87A "GIF87a"
#define GIF_HEADER_89A "GIF89a"
static int FRAME_DELAY_START_MARKER = 0x0021F904;
typedef signed char byte;
#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))
#define MINIMUM_FRAME_DELAY 2
#define DEFAULT_FRAME_DELAY 10
static inline bool isGif(void *addr) {
return !memcmp(addr, GIF_HEADER_87A, 6) || !memcmp(addr, GIF_HEADER_89A, 6);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {
byte *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
if (size < 7 || !isGif(addr)) return;
for (size_t i = 0; i < size - 8; i++) {
// TODO: Optimize this hex find with SIMD?
if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&
addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&
addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {
byte *end = addr + i + 4;
if (end[4] != 0) continue;
int frameDelay = end[2] << 8 | end[1];
if (frameDelay > MINIMUM_FRAME_DELAY)
break; // Quit if the first block looks normal, for performance
end[1] = DEFAULT_FRAME_DELAY;
end[2] = 0;
}
}
}
@@ -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.
@@ -0,0 +1,49 @@
package androidx.navigation.ui
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.navigation.ActivityNavigator
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavOptions
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
val builder = NavOptions.Builder().setLaunchSingleTop(true)
if (
navController.currentDestination!!.parent!!.findNode(item.itemId)
is ActivityNavigator.Destination
) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(com.hippo.ehviewer.R.anim.scene_open_enter)
.setExitAnim(com.hippo.ehviewer.R.anim.scene_open_exit)
.setPopEnterAnim(com.hippo.ehviewer.R.anim.scene_close_enter)
.setPopExitAnim(com.hippo.ehviewer.R.anim.scene_close_exit)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
navController.graph.findStartDestination().id,
inclusive = false,
saveState = false
)
}
val options = builder.build()
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
// Return true only if the destination we've navigated to matches the MenuItem
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
@@ -0,0 +1,25 @@
package androidx.swiperefreshlayout.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.ShapeDrawable
import android.util.AttributeSet
import androidx.core.graphics.ColorUtils
import rikka.core.res.resolveColor
@SuppressLint("PrivateResource")
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : SwipeRefreshLayout(context, attrs) {
init {
val surfaceColor =
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
val overlayColor =
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
}
}
@@ -0,0 +1,160 @@
package com.davemorrissey.labs.subscaleview
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
import java.io.InputStream
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
* use of a bitmap, asset, resource, external file or any other URI.
*
*
* When you are using a preview image, you must set the dimensions of the full size image on the
* ImageSource object for the full size image using the [.dimensions] method.
*/
class ImageSource {
val bitmap: Bitmap?
val provider: InputProvider?
private var sWidth = 0
private var sHeight = 0
var sRegion = Rect(0, 0, 0, 0)
private set
var isCached = false
private set
private constructor(bitmap: Bitmap, cached: Boolean) {
this.bitmap = bitmap
provider = null
sWidth = bitmap.width
sHeight = bitmap.height
isCached = cached
}
private constructor(provider: InputProvider) {
bitmap = null
this.provider = provider
}
/**
* Use a region of the source image. Region must be set independently for the full size image and the preview if
* you are using one.
*
* @param sRegion the region of the source image to be displayed.
* @return this instance for chaining.
*/
fun region(sRegion: Rect): ImageSource {
this.sRegion = sRegion
setInvariants()
return this
}
/**
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
*
* @param sWidth width of the source image.
* @param sHeight height of the source image.
* @return this instance for chaining.
*/
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
if (bitmap == null) {
this.sWidth = sWidth
this.sHeight = sHeight
}
setInvariants()
return this
}
private fun setInvariants() {
if (!sRegion.isEmpty) {
sWidth = sRegion.width()
sHeight = sRegion.height()
}
}
companion object {
/**
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
*
* @param resId resource ID.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun resource(context: Context, resId: Int): ImageSource {
return ImageSource(ResourceInputProvider(context, resId))
}
/**
* Create an instance from an asset name.
*
* @param assetName asset name.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun asset(context: Context, assetName: String): ImageSource {
return ImageSource(AssetInputProvider(context, assetName))
}
/**
* Create an instance from a URI.
*
* @param uri image URI.
* @return an [ImageSource] instance.
*/
fun uri(context: Context, uri: Uri): ImageSource {
return ImageSource(UriInputProvider(context, uri))
}
/**
* Create an instance from an input provider.
*
* @param provider input stream provider.
* @return an [ImageSource] instance.
*/
fun provider(provider: InputProvider): ImageSource {
return ImageSource(provider)
}
/**
* Create an instance from an input stream.
*
* @param stream open input stream.
* @return an [ImageSource] instance.
*/
fun inputStream(stream: InputStream): ImageSource {
return ImageSource(OpenStreamProvider(stream))
}
/**
* Provide a loaded bitmap for display.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun bitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, false)
}
/**
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
* or Volley.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun cachedBitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, true)
}
}
}
@@ -0,0 +1,15 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.PointF
import java.io.Serializable
/**
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
*/
class ImageViewState(val scale: Float, center: PointF) : Serializable {
private val centerX: Float = center.x
private val centerY: Float = center.y
val center: PointF
get() = PointF(centerX, centerY)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,53 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import java.lang.Exception
/**
* Interface for image decoding classes.
*/
interface Decoder {
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
fun init(context: Context, provider: InputProvider): Point
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
val isReady: Boolean
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
fun recycle()
}
@@ -0,0 +1,64 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.lang.Exception
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
bitmapConfig: Bitmap.Config? = null
) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
}
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
override fun init(context: Context, provider: InputProvider): Point {
TODO()
}
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
TODO()
}
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
override val isReady: Boolean = TODO()
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
override fun recycle() {
TODO()
}
}
@@ -0,0 +1,11 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
private val assets = context.assets
@Throws(IOException::class)
override fun openStream() = assets.open(assetName)
}
@@ -0,0 +1,10 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
interface InputProvider {
@Throws(IOException::class)
fun openStream(): InputStream?
}
@@ -0,0 +1,16 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
/**
* Provider from an already open input stream.
*
*
* Please keep in mind this provider is not reusable.
*/
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
@Throws(IOException::class)
override fun openStream() = stream
}
@@ -0,0 +1,11 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
private val resources = context.resources
@Throws(IOException::class)
override fun openStream() = resources.openRawResource(resource)
}
@@ -0,0 +1,13 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import android.net.Uri
import java.io.IOException
import kotlin.Throws
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
private val resolver = context.contentResolver
@Throws(IOException::class)
override fun openStream() = resolver.openInputStream(uri)
}
@@ -15,20 +15,22 @@
* 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
import java.nio.ByteBuffer
public class Native {
public static void initialize() {
System.loadLibrary("ehviewer");
object Native {
fun initialize() {
System.loadLibrary("ehviewer")
}
public static native String getlibarchiveVersion();
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
public static native String getliblzmaVersion();
@JvmStatic
external fun mapFd(fd: Int, capability: Long): ByteBuffer?
public static native String getzlibVersion();
public static native String getnettleVersion();
@JvmStatic
external fun unmapDirectByteBuffer(buffer: ByteBuffer)
}
@@ -0,0 +1,62 @@
/*
* 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 com.hippo.image.Image
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
private external fun extractToByteBuffer(index: Int): 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()
}
fun getImageSource(index: Int): Image.ByteBufferSource? {
val buffer = extractToByteBuffer(index)
buffer ?: return null
check(buffer.isDirect)
return object : Image.ByteBufferSource {
override fun getByteBuffer(): ByteBuffer {
return buffer
}
override fun close() {
releaseByteBuffer(buffer)
}
}
}
companion object {
@JvmStatic
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
@@ -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);
}
}
}
@@ -1,985 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.beerbelly;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
* entry has a string key and a fixed number of values. Each key must match
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
* accessible as streams or files. Each value must be between {@code 0} and
* {@code Integer.MAX_VALUE} bytes in length.
*
* <p>The cache stores its data in a directory on the filesystem. This
* directory must be exclusive to the cache; the cache may delete or overwrite
* files from its directory. It is an error for multiple processes to use the
* same cache directory at the same time.
*
* <p>This cache limits the number of bytes that it will store on the
* filesystem. When the number of stored bytes exceeds the limit, the cache will
* remove entries in the background until the limit is satisfied. The limit is
* not strict: the cache may temporarily exceed it while waiting for files to be
* deleted. The limit does not include filesystem overhead or the cache
* journal so space-sensitive applications should set a conservative limit.
*
* <p>Clients call {@link #edit} to create or update the values of an entry. An
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
* <li>When an entry is being <strong>created</strong> it is necessary to
* supply a full set of values; the empty value should be used as a
* placeholder if necessary.
* <li>When an entry is being <strong>edited</strong>, it is not necessary
* to supply data for every value; values default to their previous
* value.
* </ul>
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
* of values as they were before or after the commit, but never a mix of values.
*
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
* observe the value at the time that {@link #get} was called. Updates and
* removals after the call do not impact ongoing reads.
*
* <p>This class is tolerant of some I/O errors. If files are missing from the
* filesystem, the corresponding entries will be dropped from the cache. If
* an error occurs while writing a cache value, the edit will fail silently.
* Callers should handle other problems by catching {@code IOException} and
* responding appropriately.
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*
* The first five lines of the journal form its header. They are the
* constant string "libcore.io.DiskLruCache", the disk cache's version,
* the application's version, the value count, and a blank line.
*
* Each of the subsequent lines in the file is a record of the state of a
* cache entry. Each line contains space-separated values: a state, a key,
* and optional state-specific values.
* o DIRTY lines track that an entry is actively being created or updated.
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
* temporary files may need to be deleted.
* o CLEAN lines track a cache entry that has been successfully published
* and may be read. A publish line is followed by the lengths of each of
* its values.
* o READ lines track accesses for LRU.
* o REMOVE lines track entries that have been deleted.
*
* The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.
*/
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
@Override
public void write(int b) {
// Eat all writes silently. Nom nom.
}
};
/**
* This cache uses a single background thread to evict entries.
*/
final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
private final File directory;
private final File journalFile;
private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion;
private final int valueCount;
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<>(0, 0.75f, true);
private long maxSize;
private long size = 0;
private Writer journalWriter;
private int redundantOpCount;
/**
* To differentiate between old and current snapshots, each entry is given
* a sequence number each time an edit is committed. A snapshot is stale if
* 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;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
} 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 static void deleteIfExists(File file) throws IOException {
if (file.exists() && !file.delete()) {
throw new IOException();
}
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
deleteIfExists(to);
}
if (!from.renameTo(to)) {
throw new IOException();
}
}
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
/**
* Computes the initial size and collects garbage as a part of opening the
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
/**
* Creates a new journal that omits redundant information. This replaces the
* current journal if it exists.
*/
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
try (Writer writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
}
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
/**
* Return {@code true} if it contains the key
*/
public synchronized boolean contain(String key) {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
return entry != null && entry.readable;
}
/**
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
* exist is not currently readable. If a value is returned, it is moved to
* the head of the LRU queue.
*/
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
// Open all streams eagerly to guarantee that we see a single published
// snapshot. If we opened streams lazily then the streams could come
// from different edits.
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
/**
* Returns an editor for the entry named {@code key}, or null if another
* edit is in progress.
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
/**
* Returns the directory where this cache stores its data.
*/
public File getDirectory() {
return directory;
}
/**
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
public synchronized long getMaxSize() {
return maxSize;
}
/**
* Changes the maximum number of bytes the cache can store and queues a job
* to trim the existing store, if necessary.
*/
public synchronized void setMaxSize(long maxSize) {
this.maxSize = maxSize;
executorService.submit(cleanupCallable);
}
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
/**
* Returns true if this cache has been closed.
*/
public synchronized boolean isClosed() {
return journalWriter == null;
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
/**
* Force buffered operations to the filesystem.
*/
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // Already closed.
}
for (Entry entry : new ArrayList<>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by
* the cache.
*/
public void delete() throws IOException {
close();
Util.deleteContents(directory);
}
private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException("keys must match regex "
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
}
}
/**
* A snapshot of the values for an entry.
*/
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
private final long[] lengths;
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
this.lengths = lengths;
}
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
* is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/**
* Returns the unbuffered stream with the value for {@code index}.
*/
public InputStream getInputStream(int index) {
return ins[index];
}
/**
* Returns the string value for {@code index}.
*/
public String getString(int index) throws IOException {
return inputStreamToString(getInputStream(index));
}
/**
* Returns the byte length of the value for {@code index}.
*/
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
/**
* Edits the values for an entry.
*/
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
private boolean committed;
private Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Returns an unbuffered input stream to read the last committed value,
* or null if no value has been committed.
*/
public InputStream newInputStream(int index) {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
return null;
}
try {
return new FileInputStream(entry.getCleanFile(index));
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns the last committed value as a string, or null if no value
* has been committed.
*/
public String getString(int index) throws IOException {
InputStream in = newInputStream(index);
return in != null ? inputStreamToString(in) : null;
}
/**
* Returns a new unbuffered output stream to write the value at
* {@code index}. If the underlying output stream encounters errors
* when writing to the filesystem, this edit will be aborted when
* {@link #commit} is called. The returned output stream does not throw
* IOExceptions.
*/
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
/**
* Sets the value at {@code index} to {@code value}.
*/
public void set(int index, String value) throws IOException {
Writer writer = null;
try {
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value);
} finally {
Util.closeQuietly(writer);
}
}
/**
* Commits this edit so it is visible to readers. This releases the
* edit lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
/**
* Aborts this edit. This releases the edit lock so another edit may be
* started on the same key.
*/
public void abort() throws IOException {
completeEdit(this, false);
}
public void abortUnlessCommitted() {
if (!committed) {
try {
abort();
} catch (IOException ignored) {
}
}
}
private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(int oneByte) {
try {
out.write(oneByte);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void write(byte[] buffer, int offset, int length) {
try {
out.write(buffer, offset, length);
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void close() {
try {
out.close();
} catch (IOException e) {
hasErrors = true;
}
}
@Override
public void flush() {
try {
out.flush();
} catch (IOException e) {
hasErrors = true;
}
}
}
}
private final class Entry {
private final String key;
/**
* Lengths of this entry's files.
*/
private final long[] lengths;
/**
* True if this entry has ever been published.
*/
private boolean readable;
/**
* The ongoing edit or null if this entry is not being edited.
*/
private Editor currentEditor;
/**
* The sequence number of the most recently committed edit to this entry.
*/
private long sequenceNumber;
private Entry(String key) {
this.key = key;
this.lengths = new long[valueCount];
}
public String getLengths() {
StringBuilder result = new StringBuilder();
for (long size : lengths) {
result.append(' ').append(size);
}
return result.toString();
}
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
try {
for (int i = 0; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
}
public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
}
}
@@ -1,522 +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.beerbelly;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SimpleDiskCache {
private static final String TAG = SimpleDiskCache.class.getSimpleName();
/**
* Disk cache is free
*/
private static final int STATE_DISK_CACHE_NONE = 0;
/**
* Disk cache is flushing or clearing
*/
private static final int STATE_DISK_CACHE_IN_USE = 1;
/**
* Disk cache is reading or writing
*/
private static final int STATE_DISK_CACHE_BUSY = 2;
private final File mCacheDir;
private final int mSize;
@NonNull
private final Map<String, CounterLock> mLockMap = new HashMap<>();
@NonNull
private final LockPool mLockPool = new LockPool();
private int mDiskCacheState;
@Nullable
private DiskLruCache mDiskLruCache;
public SimpleDiskCache(File cacheDir, int size) {
mCacheDir = cacheDir;
mSize = size;
try {
init();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable
* for using as a disk filename.
*
* @param key The key used to store the file
*/
private static String hashKeyForDisk(final String key) {
String cacheKey;
try {
final MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(key.getBytes());
cacheKey = bytesToHexString(digest.digest());
} catch (final NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
/**
* http://stackoverflow.com/questions/332079
*
* @param bytes The bytes to convert.
* @return A {@link String} converted from the bytes of a hashable key used
* to store a filename on the disk, to hex digits.
*/
private static String bytesToHexString(final byte[] bytes) {
final StringBuilder builder = new StringBuilder();
for (final byte b : bytes) {
final String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
builder.append('0');
}
builder.append(hex);
}
return builder.toString();
}
public synchronized boolean isValid() {
return mDiskLruCache != null;
}
private synchronized void init() throws IOException {
if (!isValid()) {
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
}
}
public synchronized long size() {
if (null != mDiskLruCache) {
return mDiskLruCache.size();
} else {
return -1L;
}
}
public synchronized void flush() {
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
if (null != mDiskLruCache) {
try {
mDiskLruCache.flush();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache flush", e);
}
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
public synchronized boolean clear() {
boolean result = true;
if (null == mDiskLruCache) {
return false;
}
// Wait for cache available
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mDiskCacheState = STATE_DISK_CACHE_BUSY;
try {
mDiskLruCache.delete();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache clearCache", e);
}
mDiskLruCache = null;
try {
init();
} catch (IOException e) {
Log.e(TAG, "SimpleDiskCache init", e);
result = false;
}
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
return result;
}
private synchronized CounterLock obtainLock(String key) {
// Wait for clear over
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
try {
wait();
} catch (InterruptedException e) {
// Empty
}
}
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
// Get lock from key
CounterLock lock;
lock = mLockMap.get(key);
if (lock == null) {
lock = mLockPool.get();
mLockMap.put(key, lock);
}
lock.obtain();
return lock;
}
private synchronized void releaseLock(String key, CounterLock lock) {
lock.release();
if (lock.isFree()) {
mLockMap.remove(key);
mLockPool.push(lock);
}
if (mLockMap.isEmpty()) {
mDiskCacheState = STATE_DISK_CACHE_NONE;
notifyAll();
}
}
public boolean contain(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.readLock().lock();
boolean result = mDiskLruCache.contain(diskKey);
lock.readLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean remove(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result;
try {
result = mDiskLruCache.remove(diskKey);
} catch (IOException e) {
e.printStackTrace();
result = false;
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
public boolean put(@NonNull String key, @NonNull InputStream is) {
String diskKey = hashKeyForDisk(key);
CounterLock lock = obtainLock(diskKey);
if (null == mDiskLruCache) {
releaseLock(diskKey, lock);
return false;
}
lock.writeLock().lock();
boolean result = false;
if (isValid()) {
result = putToDisk(diskKey, is);
}
lock.writeLock().unlock();
releaseLock(diskKey, lock);
return result;
}
private boolean putToDisk(String key, InputStream is) {
DiskLruCache.Editor editor = null;
OutputStream os = null;
boolean completeEdit = false;
try {
//noinspection ConstantConditions
editor = mDiskLruCache.edit(key);
if (editor == null) {
// The editor is in progress
return false;
}
os = editor.newOutputStream(0);
if (os != null) {
Util.copy(is, os);
completeEdit = true;
editor.commit();
return true;
} else {
// Can't get OutputStream
completeEdit = true;
editor.abort();
return false;
}
} catch (IOException e) {
Util.closeQuietly(os);
try {
if (!completeEdit && editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
return false;
}
}
/**
* @param key the key of the target
* @return the InputStreamPipe, <code>null</code> for missing
*/
@Nullable
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
if (contain(key)) {
String diskKey = hashKeyForDisk(key);
return new CacheInputStreamPipe(diskKey);
} else {
return null;
}
}
@NonNull
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
String diskKey = hashKeyForDisk(key);
return new CacheOutputStreamPipe(diskKey);
}
private class CacheInputStreamPipe implements InputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Snapshot mCurrentSnapshot;
private CacheInputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.readLock().lock();
}
}
@Override
public void release() {
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.readLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
InputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentSnapshot != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Snapshot snapshot;
snapshot = mDiskLruCache.get(mKey);
if (snapshot == null) {
throw new IOException("Miss the key " + mKey);
}
mCurrentSnapshot = snapshot;
return snapshot.getInputStream(0);
}
@Override
public void close() {
Util.closeQuietly(mCurrentSnapshot);
mCurrentSnapshot = null;
}
}
private class CacheOutputStreamPipe implements OutputStreamPipe {
private final String mKey;
private CounterLock mLock;
private DiskLruCache.Editor mCurrentEditor;
private CacheOutputStreamPipe(String key) {
mKey = key;
}
@Override
public void obtain() {
if (mLock == null) {
mLock = obtainLock(mKey);
mLock.writeLock().lock();
}
}
@Override
public void release() {
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it first");
}
if (mLock != null) {
mLock.writeLock().unlock();
releaseLock(mKey, mLock);
mLock = null;
}
}
@Override
public @NonNull
OutputStream open() throws IOException {
if (mLock == null) {
throw new IllegalStateException("Please obtain it first");
}
if (mCurrentEditor != null) {
throw new IllegalStateException("Please close it before reopen");
}
if (null == mDiskLruCache) {
throw new IOException("Can't find disk lru cache");
}
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
if (editor == null) {
throw new IOException("Miss the key " + mKey);
}
OutputStream os;
try {
os = editor.newOutputStream(0);
mCurrentEditor = editor;
return os;
} catch (IOException e) {
// Get trouble
try {
editor.abort();
} catch (IOException ex) {
// Ignore
}
throw e;
}
}
@Override
public void close() {
if (mCurrentEditor != null) {
try {
mCurrentEditor.commit();
} catch (IOException e) {
// Empty
}
mCurrentEditor = null;
}
}
}
private class CounterLock extends ReentrantReadWriteLock {
private int mCount;
public boolean isFree() {
return mCount == 0;
}
public void release() {
if (--mCount < 0) {
throw new IllegalStateException("Release time is more than occupy time");
}
}
public void obtain() {
++mCount;
}
}
private class LockPool {
private static final int MAX_SIZE = 10;
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
private final int mMaxSize = MAX_SIZE;
private int mSize;
public void push(CounterLock lock) {
if (lock != null && mSize < mMaxSize) {
mArray[mSize++] = lock;
}
}
public CounterLock get() {
if (mSize > 0) {
CounterLock lock = mArray[--mSize];
mArray[mSize] = null;
return lock;
} else {
return new CounterLock();
}
}
}
}
@@ -1,198 +0,0 @@
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.beerbelly;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
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;
/**
* Buffers input from an {@link InputStream} for reading lines.
*
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
* line at end of input is invalid and will be ignored, the caller may use {@code
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
*
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
*/
class StrictLineReader implements Closeable {
private static final byte CR = (byte) '\r';
private static final byte LF = (byte) '\n';
private final InputStream in;
private final Charset charset;
/*
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
*/
private byte[] buf;
private int pos;
private int end;
/**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
public StrictLineReader(InputStream in, Charset charset) {
this(in, 8192, charset);
}
/**
* Constructs a new {@code LineReader} with the specified capacity and charset.
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
* supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
* or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
throw new NullPointerException();
}
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
this.in = in;
this.charset = charset;
buf = new byte[capacity];
}
/**
* Closes the reader by closing the underlying {@code InputStream} and
* marking this reader as closed.
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
buf = null;
in.close();
}
}
}
/**
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
* this end of line marker is not included in the result.
*
* @return the next line from the input.
* @throws IOException for underlying {@code InputStream} errors.
* @throws EOFException for the end of source stream.
*/
public String readLine() throws IOException {
synchronized (in) {
if (buf == null) {
throw new IOException("LineReader is closed");
}
// Read more data if we are at the end of the buffered data.
// Though it's an error to read after an exception, we will let {@code fillBuf()}
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
if (pos >= end) {
fillBuf();
}
// Try to find LF in the buffered data and return the line if successful.
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());
pos = i + 1;
return res;
}
}
// Let's anticipate up to 80 characters on top of those already read.
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
@NonNull
@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.
}
}
};
while (true) {
out.write(buf, pos, end - pos);
// Mark unterminated line in case fillBuf throws EOFException or IOException.
end = -1;
fillBuf();
// Try to find LF in the buffered data and return the line if successful.
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
if (i != pos) {
out.write(buf, pos, i - pos);
}
pos = i + 1;
return out.toString();
}
}
}
}
}
public boolean hasUnterminatedLine() {
return end == -1;
}
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
if (result == -1) {
throw new EOFException();
}
pos = 0;
end = result;
}
}
@@ -1,102 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.beerbelly;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Junk drawer of utility methods.
*/
final class Util {
static final Charset US_ASCII = StandardCharsets.US_ASCII;
static final Charset UTF_8 = StandardCharsets.UTF_8;
private static final int EOF = -1;
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private Util() {
}
static String readFully(Reader reader) throws IOException {
try (reader) {
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1) {
writer.write(buffer, 0, count);
}
return writer.toString();
}
}
/**
* Deletes the contents of {@code dir}. Throws an IOException if any file
* could not be deleted, or if {@code dir} is not a readable directory.
*/
static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
deleteContents(file);
}
if (!file.delete()) {
throw new IOException("failed to delete file: " + file);
}
}
}
static void closeQuietly(/*Auto*/Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
/**
* Copy bytes from an <code>InputStream</code> to an
* <code>OutputStream</code>.
*
* @param input the InputStream
* @param output the OutputStream
* @return the number of bytes copied
*/
public static long copy(InputStream input, OutputStream output)
throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
long count = 0;
int n;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
}
@@ -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);
}
@@ -1,176 +0,0 @@
/*
* Copyright (C) 2014 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.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import com.hippo.yorozuya.MathUtils;
public class BatteryDrawable extends Drawable {
public static final int WARN_LIMIT = 15;
@SuppressWarnings("unused")
private static final String TAG = BatteryDrawable.class.getSimpleName();
private final Paint mPaint;
private final Rect mTopRect;
private final Rect mBottomRect;
private final Rect mRightRect;
private final Rect mHeadRect;
private final Rect mElectRect;
private int mColor = Color.WHITE;
private int mWarningColor = Color.RED;
private int mElect = -1;
private int mStart;
private int mStop;
public BatteryDrawable() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mTopRect = new Rect();
mBottomRect = new Rect();
mRightRect = new Rect();
mHeadRect = new Rect();
mElectRect = new Rect();
updatePaint();
}
@Override
@SuppressWarnings("SuspiciousNameCombination")
protected void onBoundsChange(Rect bounds) {
int width = bounds.width();
int height = bounds.height();
int strokeWidth = (int) (Math.sqrt(width * width + height * height) * 0.06f);
int turn1 = width * 6 / 7;
int turn2 = height / 3;
int secBottom = height - strokeWidth;
mStart = strokeWidth;
mStop = turn1 - strokeWidth;
mTopRect.set(0, 0, turn1, strokeWidth);
mBottomRect.set(0, secBottom, turn1, height);
mRightRect.set(turn1 - strokeWidth, strokeWidth, turn1, secBottom);
mHeadRect.set(turn1, turn2, width, height - turn2);
mElectRect.set(0, strokeWidth, mStop, secBottom);
}
/**
* How to draw:<br>
* |------------------------------|<br>
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
* |------------------------------|---|<br>
* |/////////////////| |//|\\\|<br>
* |/////////////////| |//|\\\|<br>
* |------------------------------|---|<br>
* |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|<br>
* |------------------------------|
*/
@Override
public void draw(Canvas canvas) {
if (mElect == -1) {
return;
}
mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f);
canvas.drawRect(mTopRect, mPaint);
canvas.drawRect(mBottomRect, mPaint);
canvas.drawRect(mRightRect, mPaint);
canvas.drawRect(mHeadRect, mPaint);
canvas.drawRect(mElectRect, mPaint);
}
private boolean isWarn() {
return mElect <= WARN_LIMIT;
}
public void setColor(int color) {
if (mColor == color) {
return;
}
mColor = color;
if (!isWarn()) {
mPaint.setColor(mColor);
invalidateSelf();
}
}
public void setWarningColor(int color) {
if (mWarningColor == color) {
return;
}
mWarningColor = color;
if (isWarn()) {
mPaint.setColor(mWarningColor);
invalidateSelf();
}
}
public void setElect(int elect) {
if (mElect == elect) {
return;
}
mElect = elect;
updatePaint();
}
public void setElect(int elect, boolean warn) {
if (mElect == elect) {
return;
}
mElect = elect;
updatePaint(warn);
}
private void updatePaint() {
updatePaint(isWarn());
}
private void updatePaint(boolean warn) {
if (warn) {
mPaint.setColor(mWarningColor);
} else {
mPaint.setColor(mColor);
}
invalidateSelf();
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
@@ -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) {
@@ -16,7 +16,6 @@
package com.hippo.ehviewer;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -25,7 +24,6 @@ import android.os.Debug;
import androidx.annotation.NonNull;
import com.hippo.scene.StageActivity;
import com.hippo.util.PackageUtils;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
@@ -78,27 +76,10 @@ public final class Crash {
// Runtime
String topActivityClazzName = "null";
String topSceneClazzName = "null";
try {
Activity topActivity = ((EhApplication) context.getApplicationContext()).getTopActivity();
if (null != topActivity) {
topActivityClazzName = topActivity.getClass().getName();
if (topActivity instanceof StageActivity) {
Class<?> clazz = ((StageActivity) topActivity).getTopSceneClass();
if (clazz != null) {
topSceneClazzName = clazz.getName();
}
}
}
} catch (Throwable e) {
// Ignore
}
fw.write("======== Runtime ========\r\n");
fw.write("TopActivity=");
fw.write(avoidNull(topActivityClazzName));
fw.write("\r\n");
fw.write("TopScene=");
fw.write(avoidNull(topSceneClazzName));
fw.write("\r\n");
fw.write("\r\n");
@@ -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,352 @@
/*
* 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.app.ActivityManager
import android.app.Application
import android.content.ComponentCallbacks2
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.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import com.hippo.Native
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhTagDatabase
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.dao.buildMainDB
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.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import java.net.Proxy
import java.security.KeyStore
import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap<Int, Any>()
private val mActivityList = ArrayList<Activity>()
val topActivity: EhActivity?
get() = if (mActivityList.isNotEmpty()) {
mActivityList[mActivityList.size - 1] as EhActivity
} else {
null
}
fun recreateAllActivity() {
mActivityList.forEach { it.recreate() }
}
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("StaticFieldLeak")
override fun onCreate() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
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<Application>.onCreate()
Native.initialize()
GetText.initialize(this)
Settings.initialize()
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
BitmapUtils.initialize(this)
EhTagDatabase.update()
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
launchIO {
launchIO {
ehDatabase
}
launchIO {
downloadManager
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
}
private fun cleanupDownload() {
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)
}
}
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
)
) {
val referer = EhUrl.REFERER_E
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
val call = okHttpClient.newCall(request)
try {
call.execute().use { response ->
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)
}
override fun onPause(owner: LifecycleOwner) {
if (!locked)
locked_last_leave_time = System.currentTimeMillis() / 1000
locked = true
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(okHttpClient)
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
// Coil spawns a new thread for every image load by default
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
}.build()
}
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 }
@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.proxy(Proxy.NO_PROXY)
}
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 {
DiskCache.Builder().directory(File(application.cacheDir, "spider_info"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
@JvmStatic
val downloadManager by lazy { DownloadManager() }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@JvmStatic
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
@JvmStatic
val readerPreferences by lazy { ReaderPreferences(AndroidPreferenceStore(application)) }
@JvmStatic
val ehDatabase by lazy { buildMainDB(application) }
}
}
+185 -431
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 final EhDatabase db = EhApplication.getEhDatabase();
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
@@ -114,220 +99,9 @@ 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
}
}
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) {
@@ -337,10 +111,34 @@ public class EhDB {
return list;
}
public static synchronized void moveDownloadInfo(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
boolean reverse = fromPosition > toPosition;
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list(offset, limit);
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
int end = reverse ? 0 : limit - 1;
long toTime = list.get(end).time;
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).time = list.get(i + step).time;
}
list.get(start).time = toTime;
dao.update(list);
}
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = sDaoSession.getDownloadsDao();
if (null != dao.load(downloadInfo.gid)) {
DownloadsDao dao = db.downloadsDao();
if (null != dao.load(downloadInfo.getGid())) {
// Update
dao.update(downloadInfo);
} else {
@@ -349,13 +147,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 +166,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 +180,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 +207,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 +226,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,51 +238,45 @@ 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("%" + query + "%");
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();
if (null == dao.load(galleryInfo.gid)) {
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.getGid())) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
info = (LocalFavoriteInfo) galleryInfo;
@@ -504,31 +295,26 @@ 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();
quickSearch.id = null;
quickSearch.time = System.currentTimeMillis();
quickSearch.id = dao.insert(quickSearch);
QuickSearchDao dao = db.quickSearchDao();
quickSearch.setId(null);
quickSearch.setTime(System.currentTimeMillis());
quickSearch.setId(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();
dao.update(quickSearch);
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
QuickSearchDao dao = db.quickSearchDao();
dao.delete(quickSearch);
}
@@ -541,9 +327,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,68 +339,62 @@ 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 PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryInfo info = dao.load(galleryInfo.gid);
if (null != info) {
// Update time
info.time = System.currentTimeMillis();
HistoryDao dao = db.historyDao();
HistoryInfo info;
if (galleryInfo instanceof HistoryInfo) {
info = (HistoryInfo) galleryInfo;
} else {
info = new HistoryInfo(galleryInfo);
}
info.time = System.currentTimeMillis();
if (null != dao.load(info.getGid())) {
dao.update(info);
} else {
// New history
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)) {
if (null == dao.load(info.getGid())) {
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 +402,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);
filter.enable = !filter.enable;
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 +421,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 +456,7 @@ public class EhDB {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
// Delete failed file
return false;
} finally {
@@ -704,6 +470,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 +486,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.getName();
for (QuickSearch q : currentQuickSearchList) {
if (ObjectUtils.equal(q.getName(), 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 +592,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) {
}
}
}
@@ -51,7 +51,7 @@ public class FavouriteStatusRouter {
for (Map<Long, GalleryInfo> map : maps.values()) {
GalleryInfo info = map.get(gid);
if (info != null) {
info.favoriteSlot = slot;
info.setFavoriteSlot(slot);
}
}
@@ -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.ehviewer;
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;
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;
}
}
}
@@ -16,7 +16,6 @@
package com.hippo.ehviewer;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
@@ -27,19 +26,13 @@ import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import com.hippo.ehviewer.client.EhConfig;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.FavListUrlBuilder;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.glgallery.GalleryView;
import com.hippo.unifile.UniFile;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.NumberUtils;
import java.io.File;
import java.util.Locale;
import java.util.Set;
@@ -50,19 +43,18 @@ 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;
public static final int THEME_BLACK = 2;
public static final String KEY_APPLY_NAV_BAR_THEME_COLOR = "apply_nav_bar_theme_color";
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";
/********************
@@ -76,53 +68,47 @@ 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";
public static final String KEY_IMAGE_RESOLUTION = "image_size";
public static final String DEFAULT_IMAGE_RESOLUTION = EhConfig.IMAGE_SIZE_AUTO;
public static final int INVALID_DEFAULT_FAV_SLOT = -2;
public static final String KEY_ENABLE_ANALYTICS = "enable_analytics";
/********************
****** Advanced
********************/
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 DEFAULT_SECURITY = "";
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
public static final String KEY_SECURITY_DELAY = "require_unlock_delay";
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
public static final int DEFAULT_READ_CACHE_SIZE = 320;
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_HIDE_HV_EVENTS = "hide_hv_events";
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
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";
private static final boolean DEFAULT_SHOW_WARNING = true;
private static final String KEY_REMOVE_IMAGE_FILES = "include_pic";
private static final boolean DEFAULT_REMOVE_IMAGE_FILES = true;
private static final String KEY_NEED_SIGN_IN = "need_sign_in";
private static final boolean DEFAULT_NEED_SIGN_IN = true;
private static final String KEY_SELECT_SITE = "select_site";
private static final boolean DEFAULT_SELECT_SITE = true;
private static final String KEY_QUICK_SEARCH_TIP = "quick_search_tip";
private static final boolean DEFAULT_QUICK_SEARCH_TIP = true;
private static final int DEFAULT_THEME = THEME_SYSTEM;
private static final boolean DEFAULT_APPLY_NAV_BAR_THEME_COLOR = true;
private static final int DEFAULT_GALLERY_SITE = 1;
private static final String KEY_LAUNCH_PAGE = "launch_page";
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";
@@ -134,41 +120,6 @@ public class Settings {
private static final boolean DEFAULT_METERED_NETWORK_WARNING = false;
private static final String KEY_APP_LINK_VERIFY_TIP = "app_link_verify_tip";
private static final boolean DEFAULT_APP_LINK_VERIFY_TIP = false;
private static final String KEY_NIGHT_MODE = "night_mode";
private static final String DEFAULT_NIGHT_MODE = "-1";
private static final String KEY_E_INK_MODE = "e_ink_mode_2";
private static final boolean DEFAULT_E_INK_MODE = false;
/********************
****** Read
********************/
private static final String KEY_SCREEN_ROTATION = "screen_rotation";
private static final int DEFAULT_SCREEN_ROTATION = 0;
private static final String KEY_READING_DIRECTION = "reading_direction";
private static final int DEFAULT_READING_DIRECTION = GalleryView.LAYOUT_RIGHT_TO_LEFT;
private static final String KEY_PAGE_SCALING = "page_scaling";
private static final int DEFAULT_PAGE_SCALING = GalleryView.SCALE_FIT;
private static final String KEY_START_POSITION = "start_position";
private static final int DEFAULT_START_POSITION = GalleryView.START_POSITION_TOP_RIGHT;
private static final String KEY_KEEP_SCREEN_ON = "keep_screen_on";
private static final boolean DEFAULT_KEEP_SCREEN_ON = false;
private static final String KEY_SHOW_CLOCK = "gallery_show_clock";
private static final boolean DEFAULT_SHOW_CLOCK = true;
private static final String KEY_SHOW_PROGRESS = "gallery_show_progress";
private static final boolean DEFAULT_SHOW_PROGRESS = true;
private static final String KEY_SHOW_BATTERY = "gallery_show_battery";
private static final boolean DEFAULT_SHOW_BATTERY = true;
private static final String KEY_SHOW_PAGE_INTERVAL = "gallery_show_page_interval";
private static final boolean DEFAULT_SHOW_PAGE_INTERVAL = true;
private static final String KEY_VOLUME_PAGE = "volume_page";
private static final boolean DEFAULT_VOLUME_PAGE = false;
private static final String KEY_REVERSE_VOLUME_PAGE = "reserve_volume_page";
private static final boolean DEFAULT_REVERSE_VOLUME_PAGE = false;
private static final String KEY_READING_FULLSCREEN = "reading_fullscreen";
private static final boolean VALUE_READING_FULLSCREEN = true;
private static final String KEY_CUSTOM_SCREEN_LIGHTNESS = "custom_screen_lightness";
private static final boolean DEFAULT_CUSTOM_SCREEN_LIGHTNESS = false;
private static final String KEY_SCREEN_LIGHTNESS = "screen_lightness";
private static final int DEFAULT_SCREEN_LIGHTNESS = 50;
private static final boolean DEFAULT_MEDIA_SCAN = false;
private static final String KEY_RECENT_DOWNLOAD_LABEL = "recent_download_label";
private static final String DEFAULT_RECENT_DOWNLOAD_LABEL = null;
@@ -182,8 +133,6 @@ public class Settings {
private static final int DEFAULT_PRELOAD_IMAGE = 5;
private static final String KEY_DOWNLOAD_ORIGIN_IMAGE = "download_origin_image";
private static final boolean DEFAULT_DOWNLOAD_ORIGIN_IMAGE = false;
private static final String KEY_READ_THEME = "read_theme";
private static final int DEFAULT_READ_THEME = 1;
/********************
****** Favorites
********************/
@@ -225,70 +174,38 @@ public class Settings {
// -1 for local, 0 - 9 for cloud favorite, other for no default fav slot
private static final String KEY_DEFAULT_FAV_SLOT = "default_favorite_2";
private static final int DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT;
/********************
****** Analytics
********************/
private static final String KEY_ASK_ANALYTICS = "ask_analytics";
private static final boolean DEFAULT_ASK_ANALYTICS = true;
private static final boolean DEFAULT_ENABLE_ANALYTICS = false;
private static final String KEY_USER_ID = "user_id";
private static final String FILENAME_USER_ID = ".user_id";
private static final int LENGTH_USER_ID = 32;
/********************
****** 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;
private static final String KEY_SAVE_CRASH_LOG = "save_crash_log";
private static final boolean DEFAULT_SAVE_CRASH_LOG = true;
private static final boolean DEFAULT_BUILT_IN_HOSTS = false;
private static final boolean DEFAULT_FRONTING = false;
private static final String DEFAULT_APP_LANGUAGE = "system";
private static final String KEY_PROXY_TYPE = "proxy_type";
private static final int DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM;
private static final String KEY_PROXY_IP = "proxy_ip";
private static final String DEFAULT_PROXY_IP = null;
private static final String KEY_PROXY_PORT = "proxy_port";
private static final int DEFAULT_PROXY_PORT = -1;
/********************
****** Guide
********************/
private static final String KEY_GUIDE_QUICK_SEARCH = "guide_quick_search";
private static final boolean DEFAULT_GUIDE_QUICK_SEARCH = true;
private static final String KEY_GUIDE_COLLECTIONS = "guide_collections";
private static final boolean DEFAULT_GUIDE_COLLECTIONS = true;
private static final String KEY_GUIDE_DOWNLOAD_THUMB = "guide_download_thumb";
private static final boolean DEFAULT_GUIDE_DOWNLOAD_THUMB = true;
private static final String KEY_GUIDE_DOWNLOAD_LABELS = "guide_download_labels";
private static final boolean DEFAULT_GUIDE_DOWNLOAD_LABELS = true;
private static final String KEY_GUIDE_GALLERY = "guide_gallery";
private static final boolean DEFAULT_GUIDE_GALLERY = true;
private static final String KEY_CLIPBOARD_TEXT_HASH_CODE = "clipboard_text_hash_code";
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
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";
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
public static boolean LIST_THUMB_SIZE_INITED = false;
public static SharedPreferences sSettingsPre;
private static int LIST_THUMB_SIZE = 40;
private static EhConfig sEhConfig;
public static void initialize(Context context) {
sContext = context.getApplicationContext();
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(sContext);
public static void initialize() {
sSettingsPre = PreferenceManager.getDefaultSharedPreferences(EhApplication.getApplication());
sEhConfig = loadEhConfig();
fixDefaultValue(context);
fixDefaultValue();
}
private static void fixDefaultValue(Context context) {
private static void fixDefaultValue() {
if ("CN".equals(Locale.getDefault().getCountry())) {
// Enable domain fronting if the country is CN
if (!sSettingsPre.contains(KEY_BUILT_IN_HOSTS)) {
@@ -305,16 +222,6 @@ public class Settings {
}
}
public static Locale getLocale() {
Locale locale = null;
String language = Settings.getAppLanguage();
if (language != null && !language.equals("system")) {
return Locale.forLanguageTag(language);
} else {
return Locale.getDefault();
}
}
private static EhConfig loadEhConfig() {
EhConfig ehConfig = new EhConfig();
ehConfig.imageSize = getImageResolution();
@@ -351,32 +258,6 @@ public class Settings {
sSettingsPre.edit().putInt(key, value).apply();
}
public static long getLong(String key, long defValue) {
try {
return sSettingsPre.getLong(key, defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putLong(String key, long value) {
sSettingsPre.edit().putLong(key, value).apply();
}
public static float getFloat(String key, float defValue) {
try {
return sSettingsPre.getFloat(key, defValue);
} catch (ClassCastException e) {
Log.d(TAG, "Get ClassCastException when get " + key + " value", e);
return defValue;
}
}
public static void putFloat(String key, float value) {
sSettingsPre.edit().putFloat(key, value).apply();
}
public static String getString(String key, String defValue) {
try {
return sSettingsPre.getString(key, defValue);
@@ -418,14 +299,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);
}
@@ -434,22 +308,10 @@ public class Settings {
putString(KEY_DISPLAY_NAME, value);
}
public static String getAvatar() {
return getString(KEY_AVATAR, DEFAULT_AVATAR);
}
public static void putAvatar(String value) {
putString(KEY_AVATAR, value);
}
public static boolean getShowWarning() {
return getBoolean(KEY_SHOW_WARNING, DEFAULT_SHOW_WARNING);
}
public static void putShowWarning(boolean value) {
putBoolean(KEY_SHOW_WARNING, value);
}
public static boolean getRemoveImageFiles() {
return getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES);
}
@@ -482,14 +344,6 @@ public class Settings {
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
}
public static void putTheme(int theme) {
putIntToStr(KEY_THEME, theme);
}
public static boolean getApplyNavBarThemeColor() {
return getBoolean(KEY_APPLY_NAV_BAR_THEME_COLOR, DEFAULT_APPLY_NAV_BAR_THEME_COLOR);
}
public static int getGallerySite() {
return getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE);
}
@@ -500,17 +354,13 @@ public class Settings {
public static String getLaunchPageGalleryListSceneAction() {
int value = getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE);
switch (value) {
default:
case 0:
return GalleryListScene.ACTION_HOMEPAGE;
case 1:
return GalleryListScene.ACTION_SUBSCRIPTION;
case 2:
return GalleryListScene.ACTION_WHATS_HOT;
case 3:
return GalleryListScene.ACTION_TOP_LIST;
}
return switch (value) {
case 0 -> GalleryListScene.ACTION_HOMEPAGE;
case 1 -> GalleryListScene.ACTION_SUBSCRIPTION;
case 2 -> GalleryListScene.ACTION_WHATS_HOT;
case 3 -> GalleryListScene.ACTION_TOP_LIST;
default -> throw new IllegalStateException("Unexpected value: " + value);
};
}
public static int getListMode() {
@@ -523,44 +373,38 @@ public class Settings {
@DimenRes
public static int getDetailSizeResId() {
switch (getDetailSize()) {
default:
case 0:
return R.dimen.gallery_list_column_width_long;
case 1:
return R.dimen.gallery_list_column_width_short;
}
return switch (getDetailSize()) {
case 0 -> R.dimen.gallery_list_column_width_long;
case 1 -> R.dimen.gallery_list_column_width_short;
default -> throw new IllegalStateException("Unexpected value: " + getDetailSize());
};
}
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 = EhApplication.getApplication().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);
}
@@ -581,32 +425,14 @@ public class Settings {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
public static void putDefaultCategories(int value) {
sEhConfig.defaultCategories = value;
sEhConfig.setDirty();
putInt(KEY_DEFAULT_CATEGORIES, value);
}
public static int getExcludedTagNamespaces() {
return getInt(KEY_EXCLUDED_TAG_NAMESPACES, DEFAULT_EXCLUDED_TAG_NAMESPACES);
}
public static void putExcludedTagNamespaces(int value) {
sEhConfig.excludedNamespaces = value;
sEhConfig.setDirty();
putInt(KEY_EXCLUDED_TAG_NAMESPACES, value);
}
public static String getExcludedLanguages() {
return getString(KEY_EXCLUDED_LANGUAGES, DEFAULT_EXCLUDED_LANGUAGES);
}
public static void putExcludedLanguages(String value) {
sEhConfig.excludedLanguages = value;
sEhConfig.setDirty();
putString(KEY_EXCLUDED_LANGUAGES, value);
}
public static boolean getMeteredNetworkWarning() {
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
}
@@ -619,152 +445,20 @@ public class Settings {
putBoolean(KEY_APP_LINK_VERIFY_TIP, value);
}
public static int getScreenRotation() {
return getIntFromStr(KEY_SCREEN_ROTATION, DEFAULT_SCREEN_ROTATION);
}
public static void putScreenRotation(int value) {
putIntToStr(KEY_SCREEN_ROTATION, value);
}
@GalleryView.LayoutMode
public static int getReadingDirection() {
return GalleryView.sanitizeLayoutMode(getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION));
}
public static void putReadingDirection(int value) {
putIntToStr(KEY_READING_DIRECTION, value);
}
@GalleryView.ScaleMode
public static int getPageScaling() {
return GalleryView.sanitizeScaleMode(getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING));
}
public static void putPageScaling(int value) {
putIntToStr(KEY_PAGE_SCALING, value);
}
@GalleryView.StartPosition
public static int getStartPosition() {
return GalleryView.sanitizeStartPosition(getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION));
}
public static void putStartPosition(int value) {
putIntToStr(KEY_START_POSITION, value);
}
public static boolean getKeepScreenOn() {
return getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON);
}
public static void putKeepScreenOn(boolean value) {
putBoolean(KEY_KEEP_SCREEN_ON, value);
}
public static boolean getShowClock() {
return getBoolean(KEY_SHOW_CLOCK, DEFAULT_SHOW_CLOCK);
}
public static void putShowClock(boolean value) {
putBoolean(KEY_SHOW_CLOCK, value);
}
public static boolean getShowProgress() {
return getBoolean(KEY_SHOW_PROGRESS, DEFAULT_SHOW_PROGRESS);
}
public static void putShowProgress(boolean value) {
putBoolean(KEY_SHOW_PROGRESS, value);
}
public static boolean getShowBattery() {
return getBoolean(KEY_SHOW_BATTERY, DEFAULT_SHOW_BATTERY);
}
public static void putShowBattery(boolean value) {
putBoolean(KEY_SHOW_BATTERY, value);
}
public static boolean getShowPageInterval() {
return getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL);
}
public static void putShowPageInterval(boolean value) {
putBoolean(KEY_SHOW_PAGE_INTERVAL, value);
}
public static boolean getVolumePage() {
return getBoolean(KEY_VOLUME_PAGE, DEFAULT_VOLUME_PAGE);
}
public static void putVolumePage(boolean value) {
putBoolean(KEY_VOLUME_PAGE, value);
}
public static boolean getReverseVolumePage() {
return getBoolean(KEY_REVERSE_VOLUME_PAGE, DEFAULT_REVERSE_VOLUME_PAGE);
}
public static void putReverseVolumePage(boolean value) {
putBoolean(KEY_REVERSE_VOLUME_PAGE, value);
}
public static boolean getReadingFullscreen() {
return getBoolean(KEY_READING_FULLSCREEN, VALUE_READING_FULLSCREEN);
}
public static void putReadingFullscreen(boolean value) {
putBoolean(KEY_READING_FULLSCREEN, value);
}
public static boolean getCustomScreenLightness() {
return getBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, DEFAULT_CUSTOM_SCREEN_LIGHTNESS);
}
public static void putCustomScreenLightness(boolean value) {
putBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, value);
}
public static int getScreenLightness() {
return getInt(KEY_SCREEN_LIGHTNESS, DEFAULT_SCREEN_LIGHTNESS);
}
public static void putScreenLightness(int value) {
putInt(KEY_SCREEN_LIGHTNESS, value);
}
public static int getReadTheme() {
return getIntFromStr(KEY_READ_THEME, DEFAULT_READ_THEME);
}
public static void putReadTheme(int value) {
putIntToStr(KEY_READ_THEME, value);
}
public static boolean getEnabledSecurity() {
return getBoolean(KEY_SEC_SECURITY, VALUE_SEC_SECURITY);
}
public static void putEnabledSecurity(boolean value) {
putBoolean(KEY_READING_FULLSCREEN, value);
}
@Nullable
public static UniFile getDownloadLocation() {
UniFile dir = null;
try {
Uri.Builder builder = new Uri.Builder();
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
dir = UniFile.fromUri(sContext, builder.build());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
UniFile dir;
Uri.Builder builder = new Uri.Builder();
builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null));
builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null));
builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null));
builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null));
builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null));
dir = UniFile.fromUri(EhApplication.getApplication(), builder.build());
return dir != null ? dir : UniFile.fromFile(AppConfig.getDefaultDownloadDir());
}
@@ -815,26 +509,14 @@ public class Settings {
return getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD);
}
public static void putMultiThreadDownload(int value) {
putIntToStr(KEY_MULTI_THREAD_DOWNLOAD, value);
}
public static int getDownloadDelay() {
return getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY);
}
public static void putDownloadDelay(int value) {
putIntToStr(KEY_DOWNLOAD_DELAY, value);
}
public static int getPreloadImage() {
return getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE);
}
public static void putPreloadImage(int value) {
putIntToStr(KEY_PRELOAD_IMAGE, value);
}
public static String getImageResolution() {
return getString(KEY_IMAGE_RESOLUTION, DEFAULT_IMAGE_RESOLUTION);
}
@@ -849,10 +531,6 @@ public class Settings {
return getBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE);
}
public static void putDownloadOriginImage(boolean value) {
putBoolean(KEY_DOWNLOAD_ORIGIN_IMAGE, value);
}
public static String[] getFavCat() {
String[] favCat = new String[10];
favCat[0] = sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0);
@@ -947,104 +625,18 @@ public class Settings {
putInt(KEY_DEFAULT_FAV_SLOT, value);
}
public static boolean getAskAnalytics() {
return getBoolean(KEY_ASK_ANALYTICS, DEFAULT_ASK_ANALYTICS);
public static boolean getQSSaveProgress() {
return getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS);
}
public static void putAskAnalytics(boolean value) {
putBoolean(KEY_ASK_ANALYTICS, value);
}
public static boolean getEnableAnalytics() {
return getBoolean(KEY_ENABLE_ANALYTICS, DEFAULT_ENABLE_ANALYTICS);
}
public static void putEnableAnalytics(boolean value) {
putBoolean(KEY_ENABLE_ANALYTICS, value);
}
public static String getUserID() {
boolean writeXml = false;
boolean writeFile = false;
String userID = getString(KEY_USER_ID, null);
File file = AppConfig.getFileInExternalAppDir(FILENAME_USER_ID);
if (null == userID || !isValidUserID(userID)) {
writeXml = true;
// Get use ID from out sd card file
userID = FileUtils.read(file);
if (null == userID || !isValidUserID(userID)) {
writeFile = true;
userID = generateUserID();
}
} else {
writeFile = true;
}
if (writeXml) {
putString(KEY_USER_ID, userID);
}
if (writeFile) {
FileUtils.write(file, userID);
}
return userID;
}
@NonNull
private static String generateUserID() {
int length = LENGTH_USER_ID;
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
if (MathUtils.random(0, ('9' - '0' + 1) + ('z' - 'a' + 1)) <= '9' - '0') {
sb.append((char) MathUtils.random('0', '9' + 1));
} else {
sb.append((char) MathUtils.random('a', 'z' + 1));
}
}
return sb.toString();
}
private static boolean isValidUserID(@Nullable String userID) {
if (null == userID || LENGTH_USER_ID != userID.length()) {
return false;
}
for (int i = 0; i < LENGTH_USER_ID; i++) {
char ch = userID.charAt(i);
if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z')) {
return false;
}
}
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);
}
public static int getSkipUpdateVersion() {
return getInt(KEY_SKIP_UPDATE_VERSION, DEFAULT_SKIP_UPDATE_VERSION);
}
public static void putSkipUpdateVersion(int value) {
putInt(KEY_SKIP_UPDATE_VERSION, value);
public static void putQSSaveProgress(boolean value) {
putBoolean(KEY_QS_SAVE_PROGRESS, value);
}
public static boolean getSaveParseErrorBody() {
return getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY);
}
public static void putSaveParseErrorBody(boolean value) {
putBoolean(KEY_SAVE_PARSE_ERROR_BODY, value);
}
public static boolean getSaveCrashLog() {
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
}
@@ -1057,8 +649,8 @@ public class Settings {
putBoolean(KEY_SECURITY, value);
}
public static boolean getEnableFingerprint() {
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
public static int getSecurityDelay() {
return getInt(KEY_SECURITY_DELAY, 0);
}
public static int getReadCacheSize() {
@@ -1081,14 +673,6 @@ public class Settings {
putBoolean(KEY_DOMAIN_FRONTING, value);
}
public static String getAppLanguage() {
return getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE);
}
public static void putAppLanguage(String value) {
putString(KEY_APP_LANGUAGE, value);
}
public static int getProxyType() {
return getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE);
}
@@ -1113,46 +697,6 @@ public class Settings {
putInt(KEY_PROXY_PORT, value);
}
public static boolean getGuideQuickSearch() {
return getBoolean(KEY_GUIDE_QUICK_SEARCH, DEFAULT_GUIDE_QUICK_SEARCH);
}
public static void putGuideQuickSearch(boolean value) {
putBoolean(KEY_GUIDE_QUICK_SEARCH, value);
}
public static boolean getGuideCollections() {
return getBoolean(KEY_GUIDE_COLLECTIONS, DEFAULT_GUIDE_COLLECTIONS);
}
public static void putGuideCollections(boolean value) {
putBoolean(KEY_GUIDE_COLLECTIONS, value);
}
public static boolean getGuideDownloadThumb() {
return getBoolean(KEY_GUIDE_DOWNLOAD_THUMB, DEFAULT_GUIDE_DOWNLOAD_THUMB);
}
public static void putGuideDownloadThumb(boolean value) {
putBoolean(KEY_GUIDE_DOWNLOAD_THUMB, value);
}
public static boolean getGuideDownloadLabels() {
return getBoolean(KEY_GUIDE_DOWNLOAD_LABELS, DEFAULT_GUIDE_DOWNLOAD_LABELS);
}
public static void puttGuideDownloadLabels(boolean value) {
putBoolean(KEY_GUIDE_DOWNLOAD_LABELS, value);
}
public static boolean getGuideGallery() {
return getBoolean(KEY_GUIDE_GALLERY, DEFAULT_GUIDE_GALLERY);
}
public static void putGuideGallery(boolean value) {
putBoolean(KEY_GUIDE_GALLERY, value);
}
public static int getClipboardTextHashCode() {
return getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE);
}
@@ -1168,4 +712,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);
}
}
@@ -26,13 +26,10 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
import com.hippo.ehviewer.client.EhUrlOpener;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.StageActivity;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public final class UrlOpener {
@@ -55,20 +52,20 @@ public final class UrlOpener {
if (galleryDetail != null) {
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(url);
if (result != null) {
if (result.gid == galleryDetail.gid) {
intent = new Intent(context, GalleryActivity.class);
intent.setAction(GalleryActivity.ACTION_EH);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(GalleryActivity.KEY_PAGE, result.page);
if (result.gid == galleryDetail.getGid()) {
intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(ReaderActivity.KEY_PAGE, result.page);
context.startActivity(intent);
return;
}
} else if (url.startsWith("#c")) {
try {
intent = new Intent(context, GalleryActivity.class);
intent.setAction(GalleryActivity.ACTION_EH);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(GalleryActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
intent = new Intent(context, ReaderActivity.class);
intent.setAction(ReaderActivity.ACTION_EH);
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, galleryDetail);
intent.putExtra(ReaderActivity.KEY_PAGE, Integer.parseInt(url.replace("#c", "")) - 1);
context.startActivity(intent);
return;
} catch (NumberFormatException e) {
@@ -76,16 +73,6 @@ public final class UrlOpener {
}
}
}
Announcer announcer = EhUrlOpener.parseUrl(url);
if (null != announcer) {
intent = new Intent(context, MainActivity.class);
intent.setAction(StageActivity.ACTION_START_SCENE);
intent.putExtra(StageActivity.KEY_SCENE_NAME, announcer.getClazz().getName());
intent.putExtra(StageActivity.KEY_SCENE_ARGS, announcer.getArgs());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
}
CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder();
@@ -1,212 +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.client;
import android.content.Context;
import android.os.AsyncTask;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.client.exception.CancelledException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.yorozuya.SimpleHandler;
import java.io.File;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.Call;
import okhttp3.OkHttpClient;
public class EhClient {
public static final String TAG = EhClient.class.getSimpleName();
public static final int METHOD_SIGN_IN = 0;
public static final int METHOD_GET_GALLERY_LIST = 1;
public static final int METHOD_GET_GALLERY_DETAIL = 3;
public static final int METHOD_GET_PREVIEW_SET = 4;
public static final int METHOD_GET_RATE_GALLERY = 5;
public static final int METHOD_GET_COMMENT_GALLERY = 6;
public static final int METHOD_GET_GALLERY_TOKEN = 7;
public static final int METHOD_GET_FAVORITES = 8;
public static final int METHOD_ADD_FAVORITES = 9;
public static final int METHOD_ADD_FAVORITES_RANGE = 10;
public static final int METHOD_MODIFY_FAVORITES = 11;
public static final int METHOD_GET_TORRENT_LIST = 12;
public static final int METHOD_GET_PROFILE = 14;
public static final int METHOD_VOTE_COMMENT = 15;
public static final int METHOD_IMAGE_SEARCH = 16;
public static final int METHOD_ARCHIVE_LIST = 17;
public static final int METHOD_DOWNLOAD_ARCHIVE = 18;
public static final int METHOD_VOTE_TAG = 19;
private final ThreadPoolExecutor mRequestThreadPool;
private final OkHttpClient mOkHttpClient;
public EhClient(Context context) {
mRequestThreadPool = IoThreadPoolExecutor.getInstance();
mOkHttpClient = EhApplication.getOkHttpClient(context);
}
public void execute(EhRequest request) {
if (!request.isCancelled()) {
Task task = new Task(request.getMethod(), request.getCallback(), request.getEhConfig());
task.executeOnExecutor(mRequestThreadPool, request.getArgs());
request.task = task;
} else {
request.getCallback().onCancel();
}
}
public interface Callback<E> {
void onSuccess(E result);
void onFailure(Exception e);
void onCancel();
}
public class Task extends AsyncTask<Object, Void, Object> {
private final int mMethod;
private final AtomicReference<Call> mCall = new AtomicReference<>();
private final AtomicBoolean mStop = new AtomicBoolean();
private Callback mCallback;
private EhConfig mEhConfig;
public Task(int method, Callback callback, EhConfig ehConfig) {
mMethod = method;
mCallback = callback;
mEhConfig = ehConfig;
}
// Called in Job thread
public void setCall(Call call) throws CancelledException {
if (mStop.get()) {
// Stopped Job thread
throw new CancelledException();
} else {
mCall.lazySet(call);
}
}
public EhConfig getEhConfig() {
return mEhConfig;
}
public void stop() {
if (!mStop.get()) {
mStop.lazySet(true);
if (mCallback != null) {
// TODO Avoid new runnable
SimpleHandler.getInstance().post(mCallback::onCancel);
}
Status status = getStatus();
if (status == Status.PENDING) {
cancel(false);
} else if (status == Status.RUNNING) {
// It is running, cancel call if it is created
Call call = mCall.get();
if (call != null) {
call.cancel();
}
}
// Clear
mCallback = null;
mEhConfig = null;
mCall.lazySet(null);
}
}
@Override
protected Object doInBackground(Object... params) {
try {
switch (mMethod) {
case METHOD_SIGN_IN:
return EhEngine.signIn(this, mOkHttpClient, (String) params[0], (String) params[1]);
case METHOD_GET_GALLERY_LIST:
return EhEngine.getGalleryList(this, mOkHttpClient, (String) params[0]);
case METHOD_GET_GALLERY_DETAIL:
return EhEngine.getGalleryDetail(this, mOkHttpClient, (String) params[0]);
case METHOD_GET_PREVIEW_SET:
return EhEngine.getPreviewSet(this, mOkHttpClient, (String) params[0]);
case METHOD_GET_RATE_GALLERY:
return EhEngine.rateGallery(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Float) params[4]);
case METHOD_GET_COMMENT_GALLERY:
return EhEngine.commentGallery(this, mOkHttpClient, (String) params[0], (String) params[1], (String) params[2]);
case METHOD_GET_GALLERY_TOKEN:
return EhEngine.getGalleryToken(this, mOkHttpClient, (Long) params[0], (String) params[1], (Integer) params[2]);
case METHOD_GET_FAVORITES:
return EhEngine.getFavorites(this, mOkHttpClient, (String) params[0], (Boolean) params[1]);
case METHOD_ADD_FAVORITES:
return EhEngine.addFavorites(this, mOkHttpClient, (Long) params[0], (String) params[1], (Integer) params[2], (String) params[3]);
case METHOD_ADD_FAVORITES_RANGE:
return EhEngine.addFavoritesRange(this, mOkHttpClient, (long[]) params[0], (String[]) params[1], (Integer) params[2]);
case METHOD_MODIFY_FAVORITES:
return EhEngine.modifyFavorites(this, mOkHttpClient, (String) params[0], (long[]) params[1], (Integer) params[2], (Boolean) params[3]);
case METHOD_GET_TORRENT_LIST:
return EhEngine.getTorrentList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]);
case METHOD_GET_PROFILE:
return EhEngine.getProfile(this, mOkHttpClient);
case METHOD_VOTE_COMMENT:
return EhEngine.voteComment(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (Long) params[4], (Integer) params[5]);
case METHOD_IMAGE_SEARCH:
return EhEngine.imageSearch(this, mOkHttpClient, (File) params[0], (Boolean) params[1], (Boolean) params[2], (Boolean) params[3]);
case METHOD_ARCHIVE_LIST:
return EhEngine.getArchiveList(this, mOkHttpClient, (String) params[0], (Long) params[1], (String) params[2]);
case METHOD_DOWNLOAD_ARCHIVE:
return EhEngine.downloadArchive(this, mOkHttpClient, (Long) params[0], (String) params[1], (String) params[2], (String) params[3]);
case METHOD_VOTE_TAG:
return EhEngine.voteTag(this, mOkHttpClient, (Long) params[0], (String) params[1], (Long) params[2], (String) params[3], (String) params[4], (Integer) params[5]);
default:
return new IllegalStateException("Can't detect method " + mMethod);
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return e;
}
}
@SuppressWarnings("unchecked")
@Override
protected void onPostExecute(Object result) {
if (mCallback != null) {
//noinspection StatementWithEmptyBody
if (!(result instanceof CancelledException)) {
if (result instanceof Exception) {
mCallback.onFailure((Exception) result);
} else {
mCallback.onSuccess(result);
}
} else {
// onCancel is called in stop
}
}
// Clear
mCallback = null;
mEhConfig = null;
mCall.lazySet(null);
}
}
}
@@ -0,0 +1,184 @@
/*
* 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.client
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import java.io.File
object EhClient {
internal fun enqueue(request: EhRequest, scope: CoroutineScope) {
check(!request.isActive) // Abort if attempt to execute an active request
request.job = scope.launchIO {
val callback: Callback<Any?>? = request.callback
try {
val result: Any? = request.run {
if (args == null)
execute(method)
else
execute(method, *args!!)
}
withUIContext { callback?.onSuccess(result) }
} catch (e: Exception) {
if (e is CancellationException)
throw e // Don't catch coroutine CancellationException
e.printStackTrace()
withUIContext { callback?.onFailure(e) }
}
}
}
suspend fun execute(method: Int, vararg params: Any?): Any? {
return when (method) {
METHOD_SIGN_IN -> EhEngine.signIn(
params[0] as String,
params[1] as String
)
METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(
params[0] as String
)
METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(
params[0] as String?
)
METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(
params[0] as String?
)
METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Float
)
METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(
params[0] as String?,
params[1] as String,
params[2] as String?
)
METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(
params[0] as Long,
params[1] as String?,
params[2] as Int
)
METHOD_GET_FAVORITES -> EhEngine.getFavorites(
params[0] as String
)
METHOD_ADD_FAVORITES -> EhEngine.addFavorites(
params[0] as Long,
params[1] as String?,
params[2] as Int,
params[3] as String?
)
METHOD_ADD_FAVORITES_RANGE -> @Suppress("UNCHECKED_CAST") EhEngine.addFavoritesRange(
params[0] as LongArray,
params[1] as Array<String?>,
params[2] as Int
)
METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(
params[0] as String,
params[1] as LongArray,
params[2] as Int
)
METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(
params[0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_GET_PROFILE -> EhEngine.getProfile()
METHOD_VOTE_COMMENT -> EhEngine.voteComment(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as Long,
params[5] as Int
)
METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(
params[0] as File,
params[1] as Boolean,
params[2] as Boolean,
params[3] as Boolean
)
METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(
params[0] as String?,
params[1] as Long,
params[2] as String?
)
METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(
params[0] as Long,
params[1] as String?,
params[2] as String?,
params[3] as String?,
params[4] as Boolean
)
METHOD_VOTE_TAG -> EhEngine.voteTag(
params[0] as Long,
params[1] as String?,
params[2] as Long,
params[3] as String?,
params[4] as String?,
params[5] as Int
)
METHOD_GET_UCONFIG -> EhEngine.getUConfig()
else -> throw IllegalStateException("Can't detect method $method")
}
}
interface Callback<E> {
fun onSuccess(result: E)
fun onFailure(e: Exception)
fun onCancel()
}
const val METHOD_SIGN_IN = 0
const val METHOD_GET_GALLERY_LIST = 1
const val METHOD_GET_GALLERY_DETAIL = 3
const val METHOD_GET_PREVIEW_SET = 4
const val METHOD_GET_RATE_GALLERY = 5
const val METHOD_GET_COMMENT_GALLERY = 6
const val METHOD_GET_GALLERY_TOKEN = 7
const val METHOD_GET_FAVORITES = 8
const val METHOD_ADD_FAVORITES = 9
const val METHOD_ADD_FAVORITES_RANGE = 10
const val METHOD_MODIFY_FAVORITES = 11
const val METHOD_GET_TORRENT_LIST = 12
const val METHOD_GET_PROFILE = 14
const val METHOD_VOTE_COMMENT = 15
const val METHOD_IMAGE_SEARCH = 16
const val METHOD_ARCHIVE_LIST = 17
const val METHOD_DOWNLOAD_ARCHIVE = 18
const val METHOD_VOTE_TAG = 19
const val METHOD_GET_UCONFIG = 20
}
@@ -1,116 +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.client;
import android.content.Context;
import androidx.annotation.NonNull;
import com.hippo.network.CookieRepository;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class EhCookieStore extends CookieRepository {
public static final String KEY_IPD_MEMBER_ID = "ipb_member_id";
public static final String KEY_IPD_PASS_HASH = "ipb_pass_hash";
public static final String KEY_IGNEOUS = "igneous";
public static final Cookie sTipsCookie =
new Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build();
public EhCookieStore(Context context) {
super(context, "okhttp3-cookie.db");
}
public static Cookie newCookie(Cookie cookie, String newDomain, boolean forcePersistent,
boolean forceLongLive, boolean forceNotHostOnly) {
Cookie.Builder builder = new Cookie.Builder();
builder.name(cookie.name());
builder.value(cookie.value());
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE);
} else if (cookie.persistent()) {
builder.expiresAt(cookie.expiresAt());
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE);
}
if (cookie.hostOnly() && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain);
} else {
builder.domain(newDomain);
}
builder.path(cookie.path());
if (cookie.secure()) {
builder.secure();
}
if (cookie.httpOnly()) {
builder.httpOnly();
}
return builder.build();
}
public void signOut() {
clear();
}
public boolean hasSignedIn() {
HttpUrl url = HttpUrl.parse(EhUrl.HOST_E);
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH);
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
List<Cookie> cookies = super.loadForRequest(url);
boolean checkTips = domainMatch(url, EhUrl.DOMAIN_E);
if (checkTips) {
List<Cookie> result = new ArrayList<>(cookies.size() + 1);
// Add all but skip some
for (Cookie cookie : cookies) {
String name = cookie.name();
if (EhConfig.KEY_CONTENT_WARNING.equals(name)) {
continue;
}
if (EhConfig.KEY_UCONFIG.equals(name)) {
continue;
}
result.add(cookie);
}
// Add some
result.add(sTipsCookie);
return Collections.unmodifiableList(result);
} else {
return cookies;
}
}
}
@@ -0,0 +1,247 @@
/*
* 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.client
import com.hippo.ehviewer.EhApplication
import com.hippo.network.CookieDatabase
import com.hippo.network.CookieSet
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.Collections
import java.util.regex.Pattern
object EhCookieStore : CookieJar {
private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db")
private val map: MutableMap<String, CookieSet> = db.allCookies
fun signOut() {
clear()
}
fun hasSignedIn(): Boolean {
val url = EhUrl.HOST_E.toHttpUrl()
return contains(url, KEY_IPD_MEMBER_ID) &&
contains(url, KEY_IPD_PASS_HASH)
}
const val KEY_IPD_MEMBER_ID = "ipb_member_id"
const val KEY_IPD_PASS_HASH = "ipb_pass_hash"
const val KEY_IGNEOUS = "igneous"
private val sTipsCookie: Cookie = Cookie.Builder()
.name(EhConfig.KEY_CONTENT_WARNING)
.value(EhConfig.CONTENT_WARNING_NOT_SHOW)
.domain(EhUrl.DOMAIN_E)
.path("/")
.expiresAt(Long.MAX_VALUE)
.build()
fun newCookie(
cookie: Cookie, newDomain: String, forcePersistent: Boolean,
forceLongLive: Boolean, forceNotHostOnly: Boolean
): Cookie {
val builder = Cookie.Builder()
builder.name(cookie.name)
builder.value(cookie.value)
if (forceLongLive) {
builder.expiresAt(Long.MAX_VALUE)
} else if (cookie.persistent) {
builder.expiresAt(cookie.expiresAt)
} else if (forcePersistent) {
builder.expiresAt(Long.MAX_VALUE)
}
if (cookie.hostOnly && !forceNotHostOnly) {
builder.hostOnlyDomain(newDomain)
} else {
builder.domain(newDomain)
}
builder.path(cookie.path)
if (cookie.secure) {
builder.secure()
}
if (cookie.httpOnly) {
builder.httpOnly()
}
return builder.build()
}
@Synchronized
fun addCookie(cookie: Cookie) {
// For cookie database
var toAdd: Cookie? = null
var toUpdate: Cookie? = null
var toRemove: Cookie? = null
var set = map[cookie.domain]
if (set == null) {
set = CookieSet()
map[cookie.domain] = set
}
if (cookie.expiresAt <= System.currentTimeMillis()) {
toRemove = set.remove(cookie)
// If the cookie is not persistent, it's not in database
if (toRemove != null && !toRemove.persistent) {
toRemove = null
}
} else {
toAdd = cookie
toUpdate = set.add(cookie)
// If the cookie is not persistent, it's not in database
if (!toAdd.persistent) toAdd = null
if (toUpdate != null && !toUpdate.persistent) toUpdate = null
// Remove the cookie if it updates to null
if (toAdd == null && toUpdate != null) {
toRemove = toUpdate
toUpdate = null
}
}
if (toRemove != null) {
db.remove(toRemove)
}
if (toAdd != null) {
if (toUpdate != null) {
db.update(toUpdate, toAdd)
} else {
db.add(toAdd)
}
}
}
fun getCookieHeader(url: HttpUrl): String {
val cookies = getCookies(url)
val cookieHeader = StringBuilder()
var i = 0
val size = cookies.size
while (i < size) {
if (i > 0) {
cookieHeader.append("; ")
}
val cookie = cookies[i]
cookieHeader.append(cookie.name).append('=').append(cookie.value)
i++
}
return cookieHeader.toString()
}
@Synchronized
fun getCookies(url: HttpUrl): List<Cookie> {
val accepted: MutableList<Cookie> = ArrayList()
val expired: MutableList<Cookie> = ArrayList()
for ((domain, cookieSet) in map) {
if (domainMatch(url, domain)) {
cookieSet[url, accepted, expired]
}
}
for (cookie in expired) {
if (cookie.persistent) {
db.remove(cookie)
}
}
// RFC 6265 Section-5.4 step 2, sort the cookie-list
// Cookies with longer paths are listed before cookies with shorter paths.
// Ignore creation-time, we don't store them.
accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }
return accepted
}
fun contains(url: HttpUrl, name: String?): Boolean {
for (cookie in getCookies(url)) {
if (cookie.name == name) {
return true
}
}
return false
}
/**
* Remove all cookies in this `CookieRepository`.
*/
@Synchronized
fun clear() {
map.clear()
db.clear()
}
@Synchronized
fun close() {
db.close()
}
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
}
}
/**
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation
* of Android's private InetAddress#isNumeric API.
*
*
* This matches IPv6 addresses as a hex string containing at least one colon, and possibly
* including dots after the first colon. It matches IPv4 addresses as strings containing only
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict
* verification).
*/
private val VERIFY_AS_IP_ADDRESS =
Pattern.compile("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
/**
* Returns true if `host` is not a host name and might be an IP address.
*/
private fun verifyAsIpAddress(host: String): Boolean {
return VERIFY_AS_IP_ADDRESS.matcher(host).matches()
}
// okhttp3.Cookie.domainMatch(HttpUrl, String)
@JvmStatic
fun domainMatch(url: HttpUrl, domain: String?): Boolean {
val urlHost = url.host
return if (urlHost == domain) {
true // As in 'example.com' matching 'example.com'.
} else urlHost.endsWith(domain!!)
&& urlHost[urlHost.length - domain.length - 1] == '.' && !verifyAsIpAddress(
urlHost
)
// As in 'example.com' matching 'www.example.com'.
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookies = getCookies(url)
val checkTips = domainMatch(url, EhUrl.DOMAIN_E)
return if (checkTips) {
val result: MutableList<Cookie> = ArrayList(cookies.size + 1)
// Add all but skip some
for (cookie in cookies) {
val name = cookie.name
if (EhConfig.KEY_CONTENT_WARNING == name) {
continue
}
if (EhConfig.KEY_UCONFIG == name) {
continue
}
result.add(cookie)
}
// Add some
result.add(sTipsCookie)
Collections.unmodifiableList(result)
} else {
cookies
}
}
}
@@ -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
object 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", false),
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)
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,275 +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.client;
import android.util.Log;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.Filter;
import java.util.ArrayList;
import java.util.List;
public final class EhFilter {
public static final int MODE_TITLE = 0;
public static final int MODE_UPLOADER = 1;
public static final int MODE_TAG = 2;
public static final int MODE_TAG_NAMESPACE = 3;
private static final String TAG = EhFilter.class.getSimpleName();
private static EhFilter sInstance;
private final List<Filter> mTitleFilterList = new ArrayList<>();
private final List<Filter> mUploaderFilterList = new ArrayList<>();
private final List<Filter> mTagFilterList = new ArrayList<>();
private final List<Filter> mTagNamespaceFilterList = new ArrayList<>();
private EhFilter() {
List<Filter> list = EhDB.getAllFilter();
for (int i = 0, n = list.size(); i < n; i++) {
Filter filter = list.get(i);
switch (filter.mode) {
case MODE_TITLE:
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
break;
case MODE_UPLOADER:
mUploaderFilterList.add(filter);
break;
case MODE_TAG:
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
break;
case MODE_TAG_NAMESPACE:
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
break;
default:
Log.d(TAG, "Unknown mode: " + filter.mode);
break;
}
}
}
public static EhFilter getInstance() {
if (sInstance == null) {
sInstance = new EhFilter();
}
return sInstance;
}
public List<Filter> getTitleFilterList() {
return mTitleFilterList;
}
public List<Filter> getUploaderFilterList() {
return mUploaderFilterList;
}
public List<Filter> getTagFilterList() {
return mTagFilterList;
}
public List<Filter> getTagNamespaceFilterList() {
return mTagNamespaceFilterList;
}
public synchronized boolean addFilter(Filter filter) {
// enable filter by default before it is added to database
filter.enable = true;
if (!EhDB.addFilter(filter)) return false;
switch (filter.mode) {
case MODE_TITLE:
filter.text = filter.text.toLowerCase();
mTitleFilterList.add(filter);
break;
case MODE_UPLOADER:
mUploaderFilterList.add(filter);
break;
case MODE_TAG:
filter.text = filter.text.toLowerCase();
mTagFilterList.add(filter);
break;
case MODE_TAG_NAMESPACE:
filter.text = filter.text.toLowerCase();
mTagNamespaceFilterList.add(filter);
break;
default:
Log.d(TAG, "Unknown mode: " + filter.mode);
break;
}
return true;
}
public synchronized void triggerFilter(Filter filter) {
EhDB.triggerFilter(filter);
}
public synchronized void deleteFilter(Filter filter) {
EhDB.deleteFilter(filter);
switch (filter.mode) {
case MODE_TITLE:
mTitleFilterList.remove(filter);
break;
case MODE_UPLOADER:
mUploaderFilterList.remove(filter);
break;
case MODE_TAG:
mTagFilterList.remove(filter);
break;
case MODE_TAG_NAMESPACE:
mTagNamespaceFilterList.remove(filter);
break;
default:
Log.d(TAG, "Unknown mode: " + filter.mode);
break;
}
}
public synchronized boolean needTags() {
return 0 != mTagFilterList.size() || 0 != mTagNamespaceFilterList.size();
}
public synchronized boolean filterTitle(GalleryInfo info) {
if (null == info) {
return false;
}
// Title
String title = info.title;
List<Filter> filters = mTitleFilterList;
if (null != title && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && title.toLowerCase().contains(filters.get(i).text)) {
return false;
}
}
}
return true;
}
public synchronized boolean filterUploader(GalleryInfo info) {
if (null == info) {
return false;
}
// Uploader
String uploader = info.uploader;
List<Filter> filters = mUploaderFilterList;
if (null != uploader && filters.size() > 0) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && uploader.equals(filters.get(i).text)) {
return false;
}
}
}
return true;
}
private boolean matchTag(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
String tagName;
String filterNamespace;
String filterName;
int index = tag.indexOf(':');
if (index < 0) {
tagNamespace = null;
tagName = tag;
} else {
tagNamespace = tag.substring(0, index);
tagName = tag.substring(index + 1);
}
index = filter.indexOf(':');
if (index < 0) {
filterNamespace = null;
filterName = filter;
} else {
filterNamespace = filter.substring(0, index);
filterName = filter.substring(index + 1);
}
if (null != tagNamespace && null != filterNamespace &&
!tagNamespace.equals(filterNamespace)) {
return false;
}
return tagName.equals(filterName);
}
public synchronized boolean filterTag(GalleryInfo info) {
if (null == info) {
return false;
}
// Tag
String[] tags = info.simpleTags;
List<Filter> filters = mTagFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTag(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
private boolean matchTagNamespace(String tag, String filter) {
if (null == tag || null == filter) {
return false;
}
String tagNamespace;
int index = tag.indexOf(':');
if (index >= 0) {
tagNamespace = tag.substring(0, index);
return tagNamespace.equals(filter);
} else {
return false;
}
}
public synchronized boolean filterTagNamespace(GalleryInfo info) {
if (null == info) {
return false;
}
String[] tags = info.simpleTags;
List<Filter> filters = mTagNamespaceFilterList;
if (null != tags && filters.size() > 0) {
for (String tag : tags) {
for (int i = 0, n = filters.size(); i < n; i++) {
if (filters.get(i).enable && matchTagNamespace(tag, filters.get(i).text)) {
return false;
}
}
}
}
return true;
}
}
@@ -0,0 +1,315 @@
/*
* 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.client
import android.util.Log
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.Filter
import java.util.Locale
import java.util.regex.Pattern
object EhFilter {
private val mTitleFilterList: MutableList<Filter> = ArrayList()
private val mUploaderFilterList: MutableList<Filter> = ArrayList()
private val mTagFilterList: MutableList<Filter> = ArrayList()
private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()
private val mCommenterFilterList: MutableList<Filter> = ArrayList()
private val mCommentFilterList: MutableList<Filter> = ArrayList()
const val MODE_TITLE = 0
const val MODE_UPLOADER = 1
const val MODE_TAG = 2
const val MODE_TAG_NAMESPACE = 3
const val MODE_COMMENTER = 4
const val MODE_COMMENT = 5
private val TAG = EhFilter::class.java.simpleName
init {
val list = EhDB.getAllFilter()
var i = 0
val n = list.size
while (i < n) {
val filter = list[i]
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
i++
}
}
val titleFilterList: List<Filter>
get() = mTitleFilterList
val uploaderFilterList: List<Filter>
get() = mUploaderFilterList
val tagFilterList: List<Filter>
get() = mTagFilterList
val tagNamespaceFilterList: List<Filter>
get() = mTagNamespaceFilterList
val commenterFilterList: List<Filter>
get() = mCommenterFilterList
val commentFilterList: List<Filter>
get() = mCommentFilterList
@Synchronized
fun addFilter(filter: Filter): Boolean {
// enable filter by default before it is added to database
filter.enable = true
if (!EhDB.addFilter(filter)) return false
when (filter.mode) {
MODE_TITLE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTitleFilterList.add(filter)
}
MODE_TAG -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagFilterList.add(filter)
}
MODE_TAG_NAMESPACE -> {
filter.text = filter.text!!.lowercase(Locale.getDefault())
mTagNamespaceFilterList.add(filter)
}
MODE_UPLOADER -> mUploaderFilterList.add(filter)
MODE_COMMENTER -> mCommenterFilterList.add(filter)
MODE_COMMENT -> mCommentFilterList.add(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
return true
}
@Synchronized
fun triggerFilter(filter: Filter?) {
EhDB.triggerFilter(filter)
}
@Synchronized
fun deleteFilter(filter: Filter) {
EhDB.deleteFilter(filter)
when (filter.mode) {
MODE_TITLE -> mTitleFilterList.remove(filter)
MODE_TAG -> mTagFilterList.remove(filter)
MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)
MODE_UPLOADER -> mUploaderFilterList.remove(filter)
MODE_COMMENTER -> mCommenterFilterList.remove(filter)
MODE_COMMENT -> mCommentFilterList.remove(filter)
else -> Log.d(TAG, "Unknown mode: " + filter.mode)
}
}
@Synchronized
fun needTags(): Boolean {
return 0 != mTagFilterList.size || 0 != mTagNamespaceFilterList.size
}
@Synchronized
fun filterTitle(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Title
val title = info.title
val filters: List<Filter> = mTitleFilterList
if (null != title && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && title.lowercase(Locale.getDefault()).contains(
filters[i].text!!
)
) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterUploader(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Uploader
val uploader = info.uploader
val filters: List<Filter> = mUploaderFilterList
if (null != uploader && filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && uploader == filters[i].text) {
return false
}
i++
}
}
return true
}
private fun matchTag(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String?
val tagName: String
val filterNamespace: String?
val filterName: String
var index = tag.indexOf(':')
if (index < 0) {
tagNamespace = null
tagName = tag
} else {
tagNamespace = tag.substring(0, index)
tagName = tag.substring(index + 1)
}
index = filter.indexOf(':')
if (index < 0) {
filterNamespace = null
filterName = filter
} else {
filterNamespace = filter.substring(0, index)
filterName = filter.substring(index + 1)
}
return if (null != tagNamespace && null != filterNamespace &&
tagNamespace != filterNamespace
) {
false
} else tagName == filterName
}
@Synchronized
fun filterTag(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
// Tag
val tags = info.simpleTags
val filters: List<Filter> = mTagFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTag(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
private fun matchTagNamespace(tag: String?, filter: String?): Boolean {
if (null == tag || null == filter) {
return false
}
val tagNamespace: String
val index = tag.indexOf(':')
return if (index >= 0) {
tagNamespace = tag.substring(0, index)
tagNamespace == filter
} else {
false
}
}
@Synchronized
fun filterTagNamespace(info: GalleryInfo?): Boolean {
if (null == info) {
return false
}
val tags = info.simpleTags
val filters: List<Filter> = mTagNamespaceFilterList
if (null != tags && filters.isNotEmpty()) {
for (tag in tags) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {
return false
}
i++
}
}
}
return true
}
@Synchronized
fun filterCommenter(commenter: String?): Boolean {
if (null == commenter) {
return false
}
val filters: List<Filter> = mCommenterFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!! && commenter == filters[i].text) {
return false
}
i++
}
}
return true
}
@Synchronized
fun filterComment(comment: String?): Boolean {
if (null == comment) {
return false
}
val filters: List<Filter> = mCommentFilterList
if (filters.isNotEmpty()) {
var i = 0
val n = filters.size
while (i < n) {
if (filters[i].enable!!) {
val p = Pattern.compile(filters[i].text!!)
val m = p.matcher(comment)
if (m.find()) return false
}
i++
}
}
return true
}
}
@@ -1,79 +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.client;
import com.hippo.ehviewer.Settings;
public class EhRequest {
EhClient.Task task;
private int mMethod;
private Object[] mArgs;
private EhClient.Callback mCallback;
private EhConfig mEhConfig;
private boolean mCancel = false;
public int getMethod() {
return mMethod;
}
public EhRequest setMethod(int method) {
mMethod = method;
return this;
}
public Object[] getArgs() {
return mArgs;
}
public EhRequest setArgs(Object... args) {
mArgs = args;
return this;
}
public EhClient.Callback getCallback() {
return mCallback;
}
public EhRequest setCallback(EhClient.Callback callback) {
mCallback = callback;
return this;
}
public EhConfig getEhConfig() {
return mEhConfig != null ? mEhConfig : Settings.getEhConfig();
}
public EhRequest setEhConfig(EhConfig ehConfig) {
mEhConfig = ehConfig;
return this;
}
public void cancel() {
if (!mCancel) {
mCancel = true;
if (task != null) {
task.stop();
task = null;
}
}
}
public boolean isCancelled() {
return mCancel;
}
}
@@ -0,0 +1,80 @@
/*
* 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.client
import android.app.Activity
import androidx.annotation.MainThread
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
class EhRequest {
internal var job: Job? = null
val isActive
get() = job?.isActive ?: false
var method = 0
private set
var args: Array<out Any?>? = null
private set
var callback: EhClient.Callback<Any?>? = null
private set
fun setMethod(method: Int): EhRequest {
this.method = method
return this
}
fun setArgs(vararg args: Any?): EhRequest {
this.args = args
return this
}
@Suppress("UNCHECKED_CAST")
fun setCallback(callback: EhClient.Callback<*>?): EhRequest {
this.callback = callback as EhClient.Callback<Any?>?
return this
}
fun enqueue(scope: CoroutineScope) {
EhClient.enqueue(this, scope)
}
@DelicateCoroutinesApi
fun enqueue() {
EhClient.enqueue(this, GlobalScope)
}
fun enqueue(fragment: Fragment) {
enqueue(fragment.viewLifecycleOwner.lifecycleScope)
}
fun enqueue(activity: Activity) {
check(activity is FragmentActivity)
enqueue(activity.lifecycleScope)
}
@MainThread
fun cancel() {
if (isActive) {
job?.cancel()
callback?.onCancel()
}
}
}
@@ -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
object 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)
}
}
@@ -1,436 +0,0 @@
/*
* Copyright 2019 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 android.content.Context;
import android.util.Base64;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSource;
import okio.Okio;
public class EhTagDatabase {
private static final Map<String, String> NAMESPACE_TO_PREFIX = new HashMap<>();
// TODO more lock for different language
private static final Lock lock = new ReentrantLock();
private static volatile EhTagDatabase instance;
static {
NAMESPACE_TO_PREFIX.put("artist", "a:");
NAMESPACE_TO_PREFIX.put("cosplayer", "cos:");
NAMESPACE_TO_PREFIX.put("character", "c:");
NAMESPACE_TO_PREFIX.put("female", "f:");
NAMESPACE_TO_PREFIX.put("group", "g:");
NAMESPACE_TO_PREFIX.put("language", "l:");
NAMESPACE_TO_PREFIX.put("male", "m:");
NAMESPACE_TO_PREFIX.put("mixed", "x:");
NAMESPACE_TO_PREFIX.put("other", "o:");
NAMESPACE_TO_PREFIX.put("parody", "p:");
NAMESPACE_TO_PREFIX.put("reclass", "r:");
}
private final String name;
private final byte[] tags;
public EhTagDatabase(String name, BufferedSource source) throws IOException {
this.name = name;
String[] tmp;
StringBuilder buffer = new StringBuilder();
source.readInt();
for (String i : source.readUtf8().split("\n")) {
tmp = i.split("\r", 2);
buffer.append(tmp[0]);
buffer.append("\r");
try {
buffer.append(new String(Base64.decode(tmp[1], Base64.DEFAULT), StandardCharsets.UTF_8));
} catch (Exception e) {
buffer.append(tmp[1]);
}
buffer.append("\n");
}
byte[] b = buffer.toString().getBytes(StandardCharsets.UTF_8);
int totalBytes = b.length;
tags = new byte[totalBytes];
System.arraycopy(b, 0, tags, 0, totalBytes);
}
@Nullable
public static EhTagDatabase getInstance(Context context) {
if (isPossible(context)) {
return instance;
} else {
instance = null;
return null;
}
}
@Nullable
public static String namespaceToPrefix(String namespace) {
return NAMESPACE_TO_PREFIX.get(namespace);
}
private static String[] getMetadata(Context context) {
String[] metadata = context.getResources().getStringArray(R.array.tag_translation_metadata);
if (metadata.length == 4) {
return metadata;
} else {
return null;
}
}
public static boolean isPossible(Context context) {
return getMetadata(context) != null;
}
@Nullable
private static byte[] getFileContent(File file, int length) {
try (BufferedSource source = Okio.buffer(Okio.source(file))) {
byte[] content = new byte[length];
source.readFully(content);
return content;
} catch (IOException e) {
return null;
}
}
@Nullable
private static byte[] getFileSha1(File file) {
try (InputStream is = new FileInputStream(file)) {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
int n;
byte[] buffer = new byte[4 * 1024];
while ((n = is.read(buffer)) != -1) {
digest.update(buffer, 0, n);
}
return digest.digest();
} catch (IOException | NoSuchAlgorithmException e) {
return null;
}
}
private static boolean equals(byte[] b1, byte[] b2) {
if (b1 == null && b2 == null) {
return true;
}
if (b1 == null || b2 == null) {
return false;
}
if (b1.length != b2.length) {
return false;
}
for (int i = 0; i < b1.length; i++) {
if (b1[i] != b2[i]) {
return false;
}
}
return true;
}
private static boolean checkData(File sha1File, File dataFile) {
byte[] s1 = getFileContent(sha1File, 20);
if (s1 == null) {
return false;
}
byte[] s2 = getFileSha1(dataFile);
if (s2 == null) {
return false;
}
return equals(s1, s2);
}
private static boolean save(OkHttpClient client, String url, File file) {
Request request = new Request.Builder().url(url).build();
Call call = client.newCall(request);
try (Response response = call.execute()) {
if (!response.isSuccessful()) {
return false;
}
ResponseBody body = response.body();
if (body == null) {
return false;
}
try (InputStream is = body.byteStream(); OutputStream os = new FileOutputStream(file)) {
IOUtils.copy(is, os);
}
return true;
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return false;
}
}
public static void update(Context context) {
String[] urls = getMetadata(context);
if (urls == null || urls.length != 4) {
// Clear tags if it's not possible
instance = null;
return;
}
String sha1Name = urls[0];
String sha1Url = urls[1];
String dataName = urls[2];
String dataUrl = urls[3];
// Clear tags if name if different
EhTagDatabase tmp = instance;
if (tmp != null && !tmp.name.equals(dataName)) {
instance = null;
}
IoThreadPoolExecutor.getInstance().execute(() -> {
if (!lock.tryLock()) {
return;
}
try {
File dir = AppConfig.getFilesDir("tag-translations");
if (dir == null) {
return;
}
// Check current sha1 and current data
File sha1File = new File(dir, sha1Name);
File dataFile = new File(dir, dataName);
if (!checkData(sha1File, dataFile)) {
FileUtils.delete(sha1File);
FileUtils.delete(dataFile);
}
// Read current EhTagDatabase
if (instance == null && dataFile.exists()) {
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
instance = new EhTagDatabase(dataName, source);
} catch (IOException e) {
FileUtils.delete(sha1File);
FileUtils.delete(dataFile);
}
}
OkHttpClient client = EhApplication.getOkHttpClient(context);
// Save new sha1
File tempSha1File = new File(dir, sha1Name + ".tmp");
if (!save(client, sha1Url, tempSha1File)) {
FileUtils.delete(tempSha1File);
return;
}
// Check new sha1 and current data
if (checkData(tempSha1File, dataFile)) {
// The data is the same
FileUtils.delete(tempSha1File);
return;
}
// Save new data
File tempDataFile = new File(dir, dataName + ".tmp");
if (!save(client, dataUrl, tempDataFile)) {
FileUtils.delete(tempDataFile);
return;
}
// Check new sha1 and new data
if (!checkData(tempSha1File, tempDataFile)) {
FileUtils.delete(tempSha1File);
FileUtils.delete(tempDataFile);
return;
}
// Replace current sha1 and current data with new sha1 and new data
FileUtils.delete(sha1File);
FileUtils.delete(dataFile);
tempSha1File.renameTo(sha1File);
tempDataFile.renameTo(dataFile);
// Read new EhTagDatabase
try (BufferedSource source = Okio.buffer(Okio.source(dataFile))) {
instance = new EhTagDatabase(dataName, source);
} catch (IOException e) {
// Ignore
}
} finally {
lock.unlock();
}
});
}
public String getTranslation(String tag) {
return search(tags, tag.getBytes(StandardCharsets.UTF_8));
}
@Nullable
private String search(byte[] tags, byte[] tag) {
int low = 0;
int high = tags.length;
while (low < high) {
int start = (low + high) / 2;
// Look for the starting '\n'
while (start > -1 && tags[start] != '\n') {
start--;
}
start++;
// Look for the middle '\r'.
int middle = 1;
while (tags[start + middle] != '\r') {
middle++;
}
// Look for the ending '\n'
int end = middle + 1;
while (tags[start + end] != '\n') {
end++;
}
int compare;
int tagIndex = 0;
int curIndex = start;
for (; ; ) {
int tagByte = tag[tagIndex] & 0xff;
int curByte = tags[curIndex] & 0xff;
compare = tagByte - curByte;
if (compare != 0) {
break;
}
tagIndex++;
curIndex++;
if (tagIndex == tag.length && curIndex == start + middle) {
break;
}
if (tagIndex == tag.length) {
compare = -1;
break;
}
if (curIndex == start + middle) {
compare = 1;
break;
}
}
if (compare < 0) {
high = start - 1;
} else if (compare > 0) {
low = start + end + 1;
} else {
return new String(tags, start + middle + 1, end - middle - 1, StandardCharsets.UTF_8);
}
}
return null;
}
public ArrayList<Pair<String, String>> suggest(String keyword) {
return searchTag(tags, keyword);
}
private ArrayList<Pair<String, String>> searchTag(byte[] tags, String keyword) {
ArrayList<Pair<String, String>> searchHints = new ArrayList<>();
int begin = 0;
while (begin < tags.length - 1) {
int start = begin;
// Look for the starting '\n'
while (tags[start] != '\n') {
start++;
}
// Look for the middle '\r'.
int middle = 1;
while (tags[start + middle] != '\r') {
middle++;
}
// Look for the ending '\n'
int end = middle + 1;
while (tags[start + end] != '\n') {
end++;
}
begin = start + end;
byte[] hintBytes = new byte[end - middle - 1];
System.arraycopy(tags, start + middle + 1, hintBytes, 0, end - middle - 1);
String hint = new String(hintBytes, StandardCharsets.UTF_8);
byte[] tagBytes = new byte[middle];
System.arraycopy(tags, start + 1, tagBytes, 0, middle);
String tag = new String(tagBytes, StandardCharsets.UTF_8);
int index = tag.indexOf(':');
boolean keywordMatches;
if (index == -1 || index >= tag.length() - 1 || keyword.length() > 2) {
keywordMatches = containsIgnoreSpace(tag, keyword);
} else {
keywordMatches = containsIgnoreSpace(tag.substring(index + 1), keyword);
}
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
Pair<String, String> pair = new Pair<>(hint, tag);
if (!searchHints.contains(pair)) {
searchHints.add(pair);
}
}
if (searchHints.size() > 20) {
break;
}
}
return searchHints;
}
private boolean containsIgnoreSpace(String text, String key) {
return text.replace(" ", "").contains(key.replace(" ", ""));
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2019 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 android.content.Context
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.R
import com.hippo.util.ExceptionUtils
import com.hippo.util.HashCodeUtils
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.BufferedSource
import okio.buffer
import okio.source
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
object EhTagDatabase {
private lateinit var tags: JSONObject
private lateinit var tagList: ArrayList<Pair<String, String>>
fun isInitialized(): Boolean {
return this::tags.isInitialized && this::tagList.isInitialized
}
private fun updateData(source: BufferedSource) {
try {
val tmpTags = JSONObject(source.readString(StandardCharsets.UTF_8))
val tmpTagList = arrayListOf<Pair<String, String>>()
tmpTags.keys().forEachRemaining { prefix: String ->
val values = tmpTags.optJSONObject(prefix)
values?.let {
it.keys().forEachRemaining { tag: String ->
tmpTagList.add(Pair("$prefix:$tag", values.optString(tag)))
}
}
}
tags = tmpTags
tagList = tmpTagList
} catch (e: JSONException) {
e.printStackTrace()
}
}
fun getTranslation(prefix: String?, tag: String?): String? {
return tags.optJSONObject(prefix)?.optString(tag)?.trim()?.takeIf { it.isNotEmpty() }
}
/* Construct a cold flow for tag database suggestions */
fun suggestFlow(
keyword: String,
translate: Boolean,
exactly: Boolean = false
): Flow<Pair<String?, String>> = flow {
tagList.forEach {
val tag = it.first
val hint = if (translate) it.second else null
val index = tag.indexOf(':')
if (tag.substring(index + 1).trim() == keyword) {
if (exactly)
emit(Pair(hint, tag))
return@forEach
}
if (exactly)
return@forEach
val keywordMatches: Boolean =
if (index == -1 || index >= tag.length - 1 || keyword.length > 2) {
containsIgnoreSpace(tag, keyword)
} else {
containsIgnoreSpace(tag.substring(index + 1), keyword)
}
if (keywordMatches || containsIgnoreSpace(hint, keyword)) {
emit(Pair(hint, tag))
}
}
}
private fun containsIgnoreSpace(text: String?, key: String): Boolean {
return text != null && text.replace(" ", "").contains(key.replace(" ", ""))
}
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
it["artist"] = "a"
it["cosplayer"] = "cos"
it["character"] = "c"
it["female"] = "f"
it["group"] = "g"
it["language"] = "l"
it["male"] = "m"
it["mixed"] = "x"
it["other"] = "o"
it["parody"] = "p"
it["reclass"] = "r"
}
@JvmStatic
fun namespaceToPrefix(namespace: String): String? {
return NAMESPACE_TO_PREFIX[namespace]
}
private fun getMetadata(context: Context): Array<String>? {
return context.resources.getStringArray(R.array.tag_translation_metadata)
.takeIf { it.size == 4 }
}
fun isTranslatable(context: Context): Boolean {
return context.resources.getBoolean(R.bool.tag_translatable)
}
private fun getFileContent(file: File): String? {
try {
file.source().buffer().use { return it.readString(StandardCharsets.UTF_8) }
} catch (e: IOException) {
return null
}
}
private fun getFileSha1(file: File): String? {
try {
FileInputStream(file).use { stream ->
val digest = MessageDigest.getInstance("SHA-1")
var n: Int
val buffer = ByteArray(4 * 1024)
while (stream.read(buffer).also { n = it } != -1) {
digest.update(buffer, 0, n)
}
return HashCodeUtils.bytesToHexString(digest.digest())
}
} catch (e: IOException) {
return null
} catch (e: NoSuchAlgorithmException) {
return null
}
}
private fun checkData(sha1File: File, dataFile: File): Boolean {
val s1 = getFileContent(sha1File) ?: return false
val s2 = getFileSha1(dataFile) ?: return false
return s1 == s2
}
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
call.execute().use { response ->
if (!response.isSuccessful) {
return false
}
val body = response.body
body.byteStream()
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
return true
}
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}
}
@OptIn(DelicateCoroutinesApi::class)
@JvmStatic
fun update() {
launchIO {
updateInternal()
}
}
@Synchronized
private fun updateInternal() {
val urls = getMetadata(EhApplication.application)
urls?.let {
val sha1Name = urls[0]
val sha1Url = urls[1]
val dataName = urls[2]
val dataUrl = urls[3]
try {
val dir = AppConfig.getFilesDir("tag-translations")
checkNotNull(dir)
// Check current sha1 and current data
val sha1File = File(dir, sha1Name)
val dataFile = File(dir, dataName)
if (!checkData(sha1File, dataFile)) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
}
// Read current EhTagDatabase
if (!isInitialized() && dataFile.exists()) {
try {
dataFile.source().buffer().use { updateData(it) }
} catch (e: IOException) {
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
}
}
val client = okHttpClient
// Save new sha1
val tempSha1File = File(dir, "$sha1Name.tmp")
check(save(client, sha1Url, tempSha1File))
// Check new sha1 and current data
if (checkData(tempSha1File, dataFile)) {
// The data is the same
FileUtils.delete(tempSha1File)
return
}
// Save new data
val tempDataFile = File(dir, "$dataName.tmp")
check(save(client, dataUrl, tempDataFile))
// Check new sha1 and new data
if (!checkData(tempSha1File, tempDataFile)) {
FileUtils.delete(tempSha1File)
FileUtils.delete(tempDataFile)
return
}
// Replace current sha1 and current data with new sha1 and new data
FileUtils.delete(sha1File)
FileUtils.delete(dataFile)
tempSha1File.renameTo(sha1File)
tempDataFile.renameTo(dataFile)
// Read new EhTagDatabase
try {
dataFile.source().buffer().use { updateData(it) }
} catch (_: IOException) {
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
}
@@ -1,243 +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.client;
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;
public static final int SITE_EX = 1;
public static final String DOMAIN_EX = "exhentai.org";
public static final String DOMAIN_E = "e-hentai.org";
public static final String DOMAIN_LOFI = "lofi.e-hentai.org";
public static final String HOST_EX = "https://" + DOMAIN_EX + "/";
public static final String API_EX = HOST_EX + "api.php";
public static final String URL_FAVORITES_EX = HOST_EX + "favorites.php";
public static final String URL_UCONFIG_EX = HOST_EX + "uconfig.php";
public static final String URL_MY_TAGS_EX = HOST_EX + "mytags";
public static final String URL_WATCHED_EX = HOST_EX + "watched";
public static final String HOST_E = "https://" + DOMAIN_E + "/";
public static final String API_E = HOST_E + "api.php";
public static final String URL_FAVORITES_E = HOST_E + "favorites.php";
public static final String URL_UCONFIG_E = HOST_E + "uconfig.php";
public static final String URL_MY_TAGS_E = HOST_E + "mytags";
public static final String URL_WATCHED_E = HOST_E + "watched";
public static final String API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01";
public static final String URL_POPULAR_E = "https://e-hentai.org/popular";
public static final String URL_POPULAR_EX = "https://exhentai.org/popular";
public static final String URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php";
public static final String URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php";
public static final String URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login";
public static final String URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00";
public static final String URL_FORUMS = "https://forums.e-hentai.org/";
public static final String REFERER_EX = "https://" + DOMAIN_EX;
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);
}
public static String getHost() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return HOST_E;
case SITE_EX:
return HOST_EX;
}
}
public static String getFavoritesUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_FAVORITES_E;
case SITE_EX:
return URL_FAVORITES_EX;
}
}
public static String getApiUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return API_E;
case SITE_EX:
return API_EX;
}
}
public static String getReferer() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return REFERER_E;
case SITE_EX:
return REFERER_EX;
}
}
public static String getOrigin() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return ORIGIN_E;
case SITE_EX:
return ORIGIN_EX;
}
}
public static String getUConfigUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_UCONFIG_E;
case SITE_EX:
return URL_UCONFIG_EX;
}
}
public static String getMyTagsUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_MY_TAGS_E;
case SITE_EX:
return URL_MY_TAGS_EX;
}
}
public static String getGalleryDetailUrl(long gid, String token, int index, boolean allComment) {
UrlBuilder builder = new UrlBuilder(getHost() + "g/" + gid + '/' + token + '/');
if (index != 0) {
builder.addQuery("p", index);
}
if (allComment) {
builder.addQuery("hc", 1);
}
return builder.build();
}
public static String getGalleryMultiPageViewerUrl(long gid, String token) {
UrlBuilder builder = new UrlBuilder(getHost() + "mpv/" + gid + '/' + token + '/');
return builder.build();
}
public static String getPageUrl(long gid, int index, String pToken) {
return getHost() + "s/" + pToken + '/' + gid + '-' + (index + 1);
}
public static String getAddFavorites(long gid, String token) {
return getHost() + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav";
}
public static String getDownloadArchive(long gid, String token, String or) {
return getHost() + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or;
}
public static String getTagDefinitionUrl(String tag) {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_');
}
@NonNull
public static String getPopularUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_POPULAR_E;
case SITE_EX:
return URL_POPULAR_EX;
}
}
@NonNull
public static String getImageSearchUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_IMAGE_SEARCH_E;
case SITE_EX:
return URL_IMAGE_SEARCH_EX;
}
}
@NonNull
public static String getWatchedUrl() {
switch (Settings.getGallerySite()) {
default:
case SITE_E:
return URL_WATCHED_E;
case SITE_EX:
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;
}
}
}
@@ -0,0 +1,164 @@
/*
* 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.client
import com.hippo.ehviewer.Settings
import com.hippo.network.UrlBuilder
object EhUrl {
const val SITE_E = 0
const val SITE_EX = 1
const val DOMAIN_EX = "exhentai.org"
const val DOMAIN_E = "e-hentai.org"
const val DOMAIN_LOFI = "lofi.e-hentai.org"
const val HOST_EX = "https://$DOMAIN_EX/"
const val API_EX = HOST_EX + "api.php"
const val URL_FAVORITES_EX = HOST_EX + "favorites.php"
const val URL_UCONFIG_EX = HOST_EX + "uconfig.php"
const val URL_MY_TAGS_EX = HOST_EX + "mytags"
const val URL_WATCHED_EX = HOST_EX + "watched"
const val HOST_E = "https://$DOMAIN_E/"
const val API_E = HOST_E + "api.php"
const val URL_FAVORITES_E = HOST_E + "favorites.php"
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
const val URL_MY_TAGS_E = HOST_E + "mytags"
const val URL_WATCHED_E = HOST_E + "watched"
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
const val URL_POPULAR_E = "https://e-hentai.org/popular"
const val URL_POPULAR_EX = "https://exhentai.org/popular"
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
const val URL_IMAGE_SEARCH_EX = "https://exhentai.org/upload/image_lookup.php"
const val URL_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login"
const val URL_REGISTER = "https://forums.e-hentai.org/index.php?act=Reg&CODE=00"
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_FUNDS = "https://e-hentai.org/exchange.php?t=gp"
const val REFERER_EX = "https://$DOMAIN_EX"
const val ORIGIN_EX = REFERER_EX
const val REFERER_E = "https://$DOMAIN_E"
const val ORIGIN_E = REFERER_E
val host: String
get() = when (Settings.getGallerySite()) {
SITE_E -> HOST_E
SITE_EX -> HOST_EX
else -> HOST_E
}
val favoritesUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_FAVORITES_E
SITE_EX -> URL_FAVORITES_EX
else -> URL_FAVORITES_E
}
val apiUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> API_E
SITE_EX -> API_EX
else -> API_E
}
@JvmStatic
val referer: String
get() = when (Settings.getGallerySite()) {
SITE_E -> REFERER_E
SITE_EX -> REFERER_EX
else -> REFERER_E
}
val origin: String
get() = when (Settings.getGallerySite()) {
SITE_E -> ORIGIN_E
SITE_EX -> ORIGIN_EX
else -> ORIGIN_E
}
@JvmStatic
val uConfigUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_UCONFIG_E
SITE_EX -> URL_UCONFIG_EX
else -> URL_UCONFIG_E
}
@JvmStatic
val myTagsUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_MY_TAGS_E
SITE_EX -> URL_MY_TAGS_EX
else -> URL_MY_TAGS_E
}
val popularUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_POPULAR_E
SITE_EX -> URL_POPULAR_EX
else -> URL_POPULAR_E
}
val imageSearchUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_IMAGE_SEARCH_E
SITE_EX -> URL_IMAGE_SEARCH_EX
else -> URL_IMAGE_SEARCH_E
}
val watchedUrl: String
get() = when (Settings.getGallerySite()) {
SITE_E -> URL_WATCHED_E
SITE_EX -> URL_WATCHED_EX
else -> URL_WATCHED_E
}
fun getGalleryDetailUrl(gid: Long, token: String?): String {
return getGalleryDetailUrl(gid, token, 0, false)
}
@JvmStatic
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean): String {
val builder = UrlBuilder(host + "g/" + gid + '/' + token + '/')
if (index != 0) {
builder.addQuery("p", index)
}
if (allComment) {
builder.addQuery("hc", 1)
}
return builder.build()
}
@JvmStatic
fun getGalleryMultiPageViewerUrl(gid: Long, token: String): String {
val builder = UrlBuilder(host + "mpv/" + gid + '/' + token + '/')
return builder.build()
}
@JvmStatic
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
}
fun getAddFavorites(gid: Long, token: String?): String {
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
}
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
}
fun getTagDefinitionUrl(tag: String): String {
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
}
}
@@ -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.client;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.client.parser.GalleryDetailUrlParser;
import com.hippo.ehviewer.client.parser.GalleryListUrlParser;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.ehviewer.ui.scene.GalleryDetailScene;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.ehviewer.ui.scene.ProgressScene;
import com.hippo.scene.Announcer;
public class EhUrlOpener {
private static final String TAG = EhUrlOpener.class.getSimpleName();
@Nullable
public static Announcer parseUrl(String url) {
if (TextUtils.isEmpty(url)) {
return null;
}
ListUrlBuilder listUrlBuilder = GalleryListUrlParser.parse(url);
if (listUrlBuilder != null) {
Bundle args = new Bundle();
args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_LIST_URL_BUILDER);
args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder);
return new Announcer(GalleryListScene.class).setArgs(args);
}
GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(url);
if (result1 != null) {
Bundle args = new Bundle();
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN);
args.putLong(GalleryDetailScene.KEY_GID, result1.gid);
args.putString(GalleryDetailScene.KEY_TOKEN, result1.token);
return new Announcer(GalleryDetailScene.class).setArgs(args);
}
GalleryPageUrlParser.Result result2 = GalleryPageUrlParser.parse(url);
if (result2 != null) {
Bundle args = new Bundle();
args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN);
args.putLong(ProgressScene.KEY_GID, result2.gid);
args.putString(ProgressScene.KEY_PTOKEN, result2.pToken);
args.putInt(ProgressScene.KEY_PAGE, result2.page);
return new Announcer(ProgressScene.class).setArgs(args);
}
Log.i(TAG, "Can't parse url: " + url);
return null;
}
}
@@ -16,10 +16,10 @@
package com.hippo.ehviewer.client;
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 +31,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;
@@ -50,10 +48,10 @@ public class EhUtils {
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
public static final Pattern PATTERN_TITLE_PREFIX = Pattern.compile(
"^(?:(?:\\([^\\)]*\\))|(?:\\[[^\\]]*\\])|(?:\\{[^\\}]*\\})|(?:~[^~]*~)|\\s+)*");
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*");
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
public static final Pattern PATTERN_TITLE_SUFFIX = Pattern.compile(
"(?:\\s+ch.[\\s\\d-]+)?(?:(?:\\([^\\)]*\\))|(?:\\[[^\\]]*\\])|(?:\\{[^\\}]*\\})|(?:~[^~]*~)|\\s+)*$",
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
Pattern.CASE_INSENSITIVE);
private static final int[] CATEGORY_VALUES = {
@@ -67,6 +65,7 @@ public class EhUtils {
EhConfig.ASIAN_PORN,
EhConfig.NON_H,
EhConfig.WESTERN,
PRIVATE,
UNKNOWN};
private static final String[][] CATEGORY_STRINGS = {
@@ -80,6 +79,7 @@ public class EhUtils {
new String[]{"asianporn", "Asian Porn"},
new String[]{"non-h"},
new String[]{"western"},
new String[]{"private"},
new String[]{"unknown"}
};
@@ -94,6 +94,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++) {
@@ -104,48 +105,38 @@ public class EhUtils {
}
public static int getCategoryColor(int category) {
switch (category) {
case EhConfig.DOUJINSHI:
return BG_COLOR_DOUJINSHI;
case EhConfig.MANGA:
return BG_COLOR_MANGA;
case EhConfig.ARTIST_CG:
return BG_COLOR_ARTIST_CG;
case EhConfig.GAME_CG:
return BG_COLOR_GAME_CG;
case EhConfig.WESTERN:
return BG_COLOR_WESTERN;
case EhConfig.NON_H:
return BG_COLOR_NON_H;
case EhConfig.IMAGE_SET:
return BG_COLOR_IMAGE_SET;
case EhConfig.COSPLAY:
return BG_COLOR_COSPLAY;
case EhConfig.ASIAN_PORN:
return BG_COLOR_ASIAN_PORN;
case EhConfig.MISC:
return BG_COLOR_MISC;
default:
return BG_COLOR_UNKNOWN;
}
return switch (category) {
case EhConfig.DOUJINSHI -> BG_COLOR_DOUJINSHI;
case EhConfig.MANGA -> BG_COLOR_MANGA;
case EhConfig.ARTIST_CG -> BG_COLOR_ARTIST_CG;
case EhConfig.GAME_CG -> BG_COLOR_GAME_CG;
case EhConfig.WESTERN -> BG_COLOR_WESTERN;
case EhConfig.NON_H -> BG_COLOR_NON_H;
case EhConfig.IMAGE_SET -> BG_COLOR_IMAGE_SET;
case EhConfig.COSPLAY -> BG_COLOR_COSPLAY;
case EhConfig.ASIAN_PORN -> BG_COLOR_ASIAN_PORN;
case EhConfig.MISC -> BG_COLOR_MISC;
default -> BG_COLOR_UNKNOWN;
};
}
public static void signOut(Context context) {
EhApplication.getEhCookieStore(context).signOut();
public static void signOut() {
EhApplication.getEhCookieStore().signOut();
Settings.putAvatar(null);
Settings.putDisplayName(null);
Settings.putGallerySite(EhUrl.SITE_E);
Settings.putNeedSignIn(true);
}
public static boolean needSignedIn(Context context) {
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore(context).hasSignedIn();
public static boolean needSignedIn() {
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
}
public static String getSuitableTitle(GalleryInfo gi) {
if (Settings.getShowJpnTitle()) {
return TextUtils.isEmpty(gi.titleJpn) ? gi.title : gi.titleJpn;
return TextUtils.isEmpty(gi.getTitleJpn()) ? gi.getTitle() : gi.getTitleJpn();
} else {
return TextUtils.isEmpty(gi.title) ? gi.titleJpn : gi.title;
return TextUtils.isEmpty(gi.getTitle()) ? gi.getTitleJpn() : gi.getTitle();
}
}
@@ -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")
object 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)
}
}
@@ -0,0 +1,25 @@
package com.hippo.ehviewer.client.data
interface AbstractGalleryInfo {
var gid: Long
var token: String?
var title: String?
var titleJpn: String?
var thumb: String?
var category: Int
var posted: String?
var uploader: String?
var disowned: Boolean
var rating: Float
var rated: Boolean
var simpleTags: Array<String>?
var pages: Int
var thumbWidth: Int
var thumbHeight: Int
var spanSize: Int
var spanIndex: Int
var spanGroupIndex: Int
var simpleLanguage: String?
var favoriteSlot: Int
var favoriteName: String?
}
@@ -0,0 +1,89 @@
/*
* 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.client.data
import androidx.room.ColumnInfo
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@Parcelize
open class BaseGalleryInfo(
@PrimaryKey
@ColumnInfo(name = "GID")
override var gid: Long = 0,
@ColumnInfo(name = "TOKEN")
override var token: String? = null,
@ColumnInfo(name = "TITLE")
override var title: String? = null,
@ColumnInfo(name = "TITLE_JPN")
override var titleJpn: String? = null,
@ColumnInfo(name = "THUMB")
override var thumb: String? = null,
@ColumnInfo(name = "CATEGORY")
override var category: Int = 0,
@ColumnInfo(name = "POSTED")
override var posted: String? = null,
@ColumnInfo(name = "UPLOADER")
override var uploader: String? = null,
@Ignore
override var disowned: Boolean = false,
@ColumnInfo(name = "RATING")
override var rating: Float = 0f,
@Ignore
override var rated: Boolean = false,
@Ignore
override var simpleTags: Array<String>? = null,
@Ignore
override var pages: Int = 0,
@Ignore
override var thumbWidth: Int = 0,
@Ignore
override var thumbHeight: Int = 0,
@Ignore
override var spanSize: Int = 0,
@Ignore
override var spanIndex: Int = 0,
@Ignore
override var spanGroupIndex: Int = 0,
@ColumnInfo(name = "SIMPLE_LANGUAGE")
override var simpleLanguage: String? = null,
@Ignore
override var favoriteSlot: Int = -2,
@Ignore
override var favoriteName: String? = null
) : GalleryInfo
@@ -1,125 +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.client.data;
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;
public class FavListUrlBuilder implements Parcelable {
public static final int FAV_CAT_ALL = -1;
public static final int FAV_CAT_LOCAL = -2;
public static final Parcelable.Creator<FavListUrlBuilder> CREATOR = new Parcelable.Creator<FavListUrlBuilder>() {
@Override
public FavListUrlBuilder createFromParcel(Parcel source) {
return new FavListUrlBuilder(source);
}
@Override
public FavListUrlBuilder[] newArray(int size) {
return new FavListUrlBuilder[size];
}
};
private static final String TAG = FavListUrlBuilder.class.getSimpleName();
private int mIndex;
private String mKeyword;
private int mFavCat = FAV_CAT_ALL;
public FavListUrlBuilder() {
}
protected FavListUrlBuilder(Parcel in) {
this.mIndex = in.readInt();
this.mKeyword = in.readString();
this.mFavCat = in.readInt();
}
public static boolean isValidFavCat(int favCat) {
return favCat >= 0 && favCat <= 9;
}
public void setIndex(int index) {
mIndex = index;
}
public String getKeyword() {
return mKeyword;
}
public void setKeyword(String keyword) {
mKeyword = keyword;
}
public int getFavCat() {
return mFavCat;
}
public void setFavCat(int favCat) {
mFavCat = favCat;
}
public boolean isLocalFavCat() {
return mFavCat == FAV_CAT_LOCAL;
}
public String build() {
UrlBuilder ub = new UrlBuilder(EhUrl.getFavoritesUrl());
if (isValidFavCat(mFavCat)) {
ub.addQuery("favcat", Integer.toString(mFavCat));
} else if (mFavCat == FAV_CAT_ALL) {
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 (mIndex > 0) {
ub.addQuery("page", Integer.toString(mIndex));
}
return ub.build();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.mIndex);
dest.writeString(this.mKeyword);
dest.writeInt(this.mFavCat);
}
}
@@ -0,0 +1,58 @@
/*
* 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.client.data
import android.os.Parcelable
import com.hippo.ehviewer.client.EhUrl
import com.hippo.network.UrlBuilder
import com.hippo.util.encodeUTF8
import kotlinx.parcelize.Parcelize
@Parcelize
class FavListUrlBuilder(
private var mPrev: String? = null,
private var mNext: String? = null,
var jumpTo: String? = null,
var keyword: String? = null,
var favCat: Int = FAV_CAT_ALL,
) : Parcelable {
fun setIndex(index: String?, isNext: Boolean) {
mNext = index.takeIf { isNext }
mPrev = index.takeUnless { isNext }
}
fun build(): String {
val ub = UrlBuilder(EhUrl.favoritesUrl)
if (isValidFavCat(favCat)) {
ub.addQuery("favcat", favCat.toString())
} else if (favCat == FAV_CAT_ALL) {
ub.addQuery("favcat", "all")
}
keyword?.takeIf { it.isNotBlank() }?.let { ub.addQuery("f_search", encodeUTF8(it)) }
mPrev?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("prev", it) }
mNext?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("next", it) }
jumpTo?.takeIf { it.isNotEmpty() }?.let { ub.addQuery("seek", it) }
return ub.build()
}
companion object {
const val FAV_CAT_ALL = -1
const val FAV_CAT_LOCAL = -2
fun isValidFavCat(favCat: Int): Boolean {
return favCat in 0..9
}
}
}
@@ -1,98 +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.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
public class GalleryApiInfo implements Parcelable {
public static final Parcelable.Creator<GalleryApiInfo> CREATOR = new Parcelable.Creator<GalleryApiInfo>() {
@Override
public GalleryApiInfo createFromParcel(Parcel source) {
return new GalleryApiInfo(source);
}
@Override
public GalleryApiInfo[] newArray(int size) {
return new GalleryApiInfo[size];
}
};
public long gid;
public String token;
public String archiverKey;
public String title;
public String titleJpn;
public int category;
public String thumb;
public String uploader;
public long posted;
public int filecount;
public long filesize;
public boolean expunged;
public float rating;
public int torrentcount;
@Nullable
public String[] tags;
public GalleryApiInfo() {
}
protected GalleryApiInfo(Parcel in) {
this.gid = in.readLong();
this.token = in.readString();
this.archiverKey = in.readString();
this.title = in.readString();
this.titleJpn = in.readString();
this.category = in.readInt();
this.thumb = in.readString();
this.uploader = in.readString();
this.posted = in.readLong();
this.filecount = in.readInt();
this.filesize = in.readLong();
this.expunged = in.readByte() != 0;
this.rating = in.readFloat();
this.torrentcount = in.readInt();
this.tags = in.createStringArray();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(this.gid);
dest.writeString(this.token);
dest.writeString(this.archiverKey);
dest.writeString(this.title);
dest.writeString(this.titleJpn);
dest.writeInt(this.category);
dest.writeString(this.thumb);
dest.writeString(this.uploader);
dest.writeLong(this.posted);
dest.writeInt(this.filecount);
dest.writeLong(this.filesize);
dest.writeByte(expunged ? (byte) 1 : (byte) 0);
dest.writeFloat(this.rating);
dest.writeInt(this.torrentcount);
dest.writeStringArray(this.tags);
}
}
@@ -13,24 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
package com.hippo.ehviewer.ui.fragment;
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.R;
public class ReadFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.read_settings);
}
@Override
public int getFragmentTitle() {
return R.string.settings_read;
}
}
@Parcelize
class GalleryApiInfo(
var gid: Long = 0,
var token: String? = null,
var archiverKey: String? = null,
var title: String? = null,
var titleJpn: String? = null,
var category: Int = 0,
var thumb: String? = null,
var uploader: String? = null,
var posted: Long = 0,
var filecount: Int = 0,
var filesize: Long = 0,
var expunged: Boolean = false,
var rating: Float = 0f,
var torrentcount: Int = 0,
var tags: Array<String>?
) : Parcelable
@@ -1,90 +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.client.data;
import android.os.Parcel;
import android.os.Parcelable;
public class GalleryComment implements Parcelable {
public static final Creator<GalleryComment> CREATOR = new Creator<GalleryComment>() {
@Override
public GalleryComment createFromParcel(Parcel source) {
return new GalleryComment(source);
}
@Override
public GalleryComment[] newArray(int size) {
return new GalleryComment[size];
}
};
// 0 for uploader comment. can't vote
public long id;
public int score;
public boolean editable;
public boolean voteUpAble;
public boolean voteUpEd;
public boolean voteDownAble;
public boolean voteDownEd;
public boolean uploader;
public String voteState;
public long time;
public String user;
public String comment;
public long lastEdited;
public GalleryComment() {
}
protected GalleryComment(Parcel in) {
this.id = in.readLong();
this.score = in.readInt();
this.editable = in.readByte() != 0;
this.voteUpAble = in.readByte() != 0;
this.voteUpEd = in.readByte() != 0;
this.voteDownAble = in.readByte() != 0;
this.voteDownEd = in.readByte() != 0;
this.uploader = in.readByte() != 0;
this.voteState = in.readString();
this.time = in.readLong();
this.user = in.readString();
this.comment = in.readString();
this.lastEdited = in.readLong();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(this.id);
dest.writeInt(this.score);
dest.writeByte(editable ? (byte) 1 : (byte) 0);
dest.writeByte(voteUpAble ? (byte) 1 : (byte) 0);
dest.writeByte(voteUpEd ? (byte) 1 : (byte) 0);
dest.writeByte(voteDownAble ? (byte) 1 : (byte) 0);
dest.writeByte(voteDownEd ? (byte) 1 : (byte) 0);
dest.writeByte(uploader ? (byte) 1 : (byte) 0);
dest.writeString(this.voteState);
dest.writeLong(this.time);
dest.writeString(this.user);
dest.writeString(this.comment);
dest.writeLong(this.lastEdited);
}
}
@@ -0,0 +1,50 @@
/*
* 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.client.data
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class GalleryComment(
// 0 for uploader comment. can't vote
@JvmField
var id: Long = 0,
@JvmField
var score: Int = 0,
@JvmField
var editable: Boolean = false,
@JvmField
var voteUpAble: Boolean = false,
@JvmField
var voteUpEd: Boolean = false,
@JvmField
var voteDownAble: Boolean = false,
@JvmField
var voteDownEd: Boolean = false,
@JvmField
var uploader: Boolean = false,
@JvmField
var voteState: String? = null,
@JvmField
var time: Long = 0,
@JvmField
var user: String? = null,
@JvmField
var comment: String? = null,
@JvmField
var lastEdited: Long = 0
) : Parcelable
@@ -1,65 +0,0 @@
/*
* Copyright 2019 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 java.util.Arrays;
public class GalleryCommentList implements Parcelable {
public static final Creator<GalleryCommentList> CREATOR = new Creator<GalleryCommentList>() {
@Override
public GalleryCommentList createFromParcel(Parcel in) {
return new GalleryCommentList(in);
}
@Override
public GalleryCommentList[] newArray(int size) {
return new GalleryCommentList[size];
}
};
public GalleryComment[] comments;
public boolean hasMore;
public GalleryCommentList(GalleryComment[] comments, boolean hasMore) {
this.comments = comments;
this.hasMore = hasMore;
}
protected GalleryCommentList(Parcel in) {
Parcelable[] array = in.readParcelableArray(getClass().getClassLoader());
if (array != null) {
comments = Arrays.copyOf(array, array.length, GalleryComment[].class);
} else {
comments = null;
}
hasMore = in.readByte() != 0;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelableArray(comments, flags);
dest.writeByte(hasMore ? (byte) 1 : (byte) 0);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2015 Hippo Seven
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,10 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.client.data
package com.hippo.conaco;
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
public interface ProgressNotifier {
void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize);
}
@Parcelize
class GalleryCommentList(
@JvmField
var comments: Array<GalleryComment>?,
@JvmField
var hasMore: Boolean
) : Parcelable
Loaded 100 of 662 files, more files were not shown because too many files have changed in this diff. Show more