Compare commits

...
Author SHA1 Message Date
tarsin 42f36b8e74 Release 1.8.7.0-alpha
Releases / Build (push) Has been cancelled
2023-02-24 20:30:15 +08:00
tarsin 9bc3c9a902 Add Resource lock to avoid memory corruption 2023-02-24 20:02:35 +08:00
tarsin 1dd04740bb Use MADV_DONTNEED as fallback 2023-02-24 19:22:10 +08:00
FooIbar 35201e6615 Use natsort for better archive file sorting 2023-02-24 18:43:08 +08:00
tarsin 86d416bccf Don't populate archive 2023-02-24 18:33:44 +08:00
tarsin 979841665a Convert PreciselyClipDrawable to kt 2023-02-24 16:53:35 +08:00
tarsin c29b8c114b Rename .java to .kt 2023-02-24 16:53:35 +08:00
tarsin a03eae3c87 Refactor importDB! 2023-02-24 16:36:41 +08:00
tarsin 704c2a261e Use MAP_UNINITIALIZED to improve performance 2023-02-24 13:05:43 +08:00
tarsin 8fabdd1926 Suppress lint 2023-02-24 12:41:07 +08:00
tarsin f9df373a16 Some infeasible optimization 2023-02-24 12:39:58 +08:00
tarsin f959f98012 Minor cleanup 2023-02-24 12:02:42 +08:00
tarsin e12a93f36b Introduce new archive decompress mempool, Resolve #307 2023-02-24 11:37:30 +08:00
FooIbar c3aca0a88d Fix missing parentheses 2023-02-24 10:01:49 +08:00
FooIbar add7819945 Minor refactor
Revert "Fix RTL load"

This reverts commit 323d258b

Revert "Resolve #814"

This reverts commit eeed0720
2023-02-24 10:01:49 +08:00
FooIbar 3c6d3bbd80 Add commit sha and build time, drop PackageUtils 2023-02-24 10:00:45 +08:00
FooIbar 66e1315b08 Convert Crash to kotlin 2023-02-24 10:00:45 +08:00
FooIbar 9bbe010fc9 Rename .java to .kt 2023-02-24 10:00:45 +08:00
tarsin 4f81d164c8 KernelIO motto motto 2023-02-23 22:52:57 +08:00
tarsin c808e9135d Print stacktrace if failed to export
Though it is unlikely to happen
2023-02-23 22:40:28 +08:00
tarsin 1e7861ef15 Refactor exportDB! 2023-02-23 22:37:57 +08:00
tarsin a70b8c6dff Cleanup SubsamplingScaleImageView
InputStream sucks, we may add ImageDecoder impl later
2023-02-23 21:45:57 +08:00
tarsin ce86638e8e Cleanup ReaderPageImageView 2023-02-23 21:32:29 +08:00
tarsin 06ab978f53 Revert "Remove SubsamplingScaleImageView"
This reverts commit 5eda7b3fc3.
2023-02-23 21:29:04 +08:00
tarsin 9c2026982c Revert "Cleanup ReaderPageImageView"
This reverts commit c07325e71e.
2023-02-23 21:28:50 +08:00
tarsin 863b910311 Revert "Remove unused res"
This reverts commit 9462f1455b.
2023-02-23 21:28:34 +08:00
tarsin 68da3b7580 Upgrade compose compiler 2023-02-23 19:51:50 +08:00
FooIbar 43556f219b Improve login process regarding IP restriction 2023-02-23 17:55:45 +08:00
renovate[bot] 6022144dce Update dependency androidx.core:core-ktx to v1.10.0-beta01 2023-02-23 00:03:57 +00:00
tarsin 18a0e3fd9e Clarify we use concurrency, not multi-thread 2023-02-22 23:10:43 +08:00
FooIbar 6c7c6e4b22 Fix DailyCheckWork initialDelay being negative 2023-02-22 21:50:36 +08:00
tarsin ffa7a74575 Add youtrack issue link 2023-02-22 21:01:02 +08:00
tarsin 37f9c28133 Opt-in ExperimentalContractsApi globally 2023-02-22 20:34:11 +08:00
FooIbar ea7a012e0a Remove crop borders until we support it 2023-02-22 20:28:43 +08:00
tarsin 0d594da6c9 Cleanup some nullability 2023-02-22 20:26:06 +08:00
tarsin ed588bf26c Convert EhDB to kotlin 2023-02-22 19:23:49 +08:00
tarsin b6673fd828 Rename .java to .kt 2023-02-22 19:23:49 +08:00
tarsin 060bcc76ef Drop support for old db 2023-02-22 19:11:57 +08:00
tarsin 8fc8dd2960 Use version in BuildConfig 2023-02-22 19:07:22 +08:00
FooIbar 1dc36275aa Do not use notifyDataSetChanged() when refreshing WebtoonAdapter 2023-02-22 18:41:09 +08:00
tarsin f305e68202 Reland: Fix a atomic usage 2023-02-22 18:01:05 +08:00
tarsin 5c481df41f Revert "Migrate to atomicfu and fix a atomic usage"
Reason for revert: Don't use atomicfu, use platform atomic impl to
benefit from VarHandler backend
This reverts commit a0a215400e.
2023-02-22 17:58:23 +08:00
tarsin e86c626536 Revert "Replace all atomic usage"
This reverts commit a6c204e444.
2023-02-22 17:58:08 +08:00
tarsin 44559bab1c Revert "Revert "Replace all atomic usage""
This reverts commit 8b9985a7dc.
2023-02-22 17:57:53 +08:00
tarsin 2ae28f1c63 Revert "Reland: Replace all atomic usage"
This reverts commit 37f705d517.
2023-02-22 17:57:39 +08:00
tarsin b3e16d2b2a Revert "Proper way to configure atomicfu"
This reverts commit 22c8a36224.
2023-02-22 17:57:26 +08:00
tarsin 6f186080df Revert "Workaround atomicfu with r8"
This reverts commit c9d9335b71.
2023-02-22 17:57:16 +08:00
tarsin c9d9335b71 Workaround atomicfu with r8 2023-02-22 17:17:15 +08:00
tarsin 22c8a36224 Proper way to configure atomicfu 2023-02-22 16:49:20 +08:00
tarsin 37f705d517 Reland: Replace all atomic usage 2023-02-22 16:14:19 +08:00
tarsin 8b9985a7dc Revert "Replace all atomic usage"
This reverts commit a6c204e444.
2023-02-22 16:10:13 +08:00
tarsin f51e585d8d Resolve #865 2023-02-22 15:58:55 +08:00
tarsin a6c204e444 Replace all atomic usage 2023-02-22 15:35:40 +08:00
tarsin a0a215400e Migrate to atomicfu and fix a atomic usage 2023-02-22 15:30:10 +08:00
tarsin 37e3166f98 Remove all explicitly thread usage 2023-02-22 11:46:24 +08:00
tarsin ddccdc2ade Partially revert 7d87417095
DownloadManager is still piece of shit, need time to rewrite
2023-02-22 00:19:47 +08:00
tarsin 755a15d5e5 Explicitly init DownloadManager on app startup 2023-02-21 23:00:26 +08:00
tarsin 5c6716bd98 Migrate to new downloadManager accessor 2023-02-21 22:48:02 +08:00
tarsin 7d87417095 Deny mainthread db query! 2023-02-21 22:32:09 +08:00
tarsin 77bb6e9a82 Don't take SpiderQueen reference in detailFragment 2023-02-21 22:26:44 +08:00
tarsin 019174d7e2 Remove all AsyncTask usage, Resolve #631 2023-02-21 22:11:36 +08:00
tarsin cac88cbb81 Rename .java to .kt 2023-02-21 22:11:36 +08:00
FooIbar 25500da486 Set page status to QUEUE if its image needs update
Old image was recycled due to cache eviction
2023-02-21 08:56:30 +08:00
tarsin 53f2c7126b Add debug info for tracing resource leak 2023-02-21 00:43:37 +08:00
FooIbar eb95026a47 Do not launch new coroutines in invokeOnCompletion()
The scope might has been cancelled
2023-02-21 00:31:05 +08:00
tarsin 6b0f8bc325 Convert all PageLoader to kotlin 2023-02-21 00:30:19 +08:00
tarsin a80f41a89a Rename .java to .kt 2023-02-21 00:30:19 +08:00
tarsin 8690f8aa8d Rewrite ArchivePageLoader with modern kotlin coroutine and flows 2023-02-21 00:23:24 +08:00
tarsin d9c55a284b Rename .java to .kt 2023-02-21 00:23:24 +08:00
tarsin c8a10a8b75 Fixup! Fix thumb retry 2023-02-20 22:44:41 +08:00
tarsin 8e8dc76c3d Fix thumb retry 2023-02-20 22:38:53 +08:00
tarsin 725679f5d4 All refresh thumb at any time 2023-02-20 22:23:34 +08:00
tarsin dfce07d7eb Use framework's DrawableWrapper 2023-02-20 21:39:48 +08:00
FooIbar 73c9e37eba Fix failed download task won't report 2023-02-20 19:21:46 +08:00
tarsin ccd5bd8394 Sync with 1.8.6.2 versionCode 2023-02-20 19:09:02 +08:00
FooIbar fd9b7db999 Fix 509 detection false positive 2023-02-20 18:58:59 +08:00
tarsin f3fe115733 Upgrade buildTools 2023-02-20 18:52:55 +08:00
FooIbar 16f3f74a90 Clean up unfinished download files 2023-02-20 15:15:00 +08:00
FooIbar 4a6b85003a Workaround for del tag parsing in comments 2023-02-20 14:56:29 +08:00
tarsin bf651c7e40 Manage ImageView load status through coil
Will implement retry later
2023-02-20 13:32:53 +08:00
FooIbar 473ff4d308 Use initializer instead of getter for coroutineContext 2023-02-20 08:50:26 +08:00
FooIbar 5d64e8c61b fixup! Introduce kotlin style diskcache operation 2023-02-19 23:40:41 +08:00
FooIbar b559bb3904 Temporarily disable vpn bypass on login 2023-02-19 22:08:45 +08:00
tarsin 359f250b3b Introduce new CharSequence operator trimAnd 2023-02-19 21:06:28 +08:00
tarsin a20336a11a Introduce kotlin style diskcache operation 2023-02-19 20:54:34 +08:00
tarsin 0313c5705f Don't limit coil parallelism and don't allow rgb565 2023-02-19 18:23:01 +08:00
tarsin 14469638b3 Delegate all image request listener
In case request being cancelled
2023-02-19 18:14:25 +08:00
tarsin d15c5fc9d1 Implement image request merging in standalone Interceptor, Don't modify coil 2023-02-19 18:08:35 +08:00
tarsin 16e7d266fe Resolve #842 2023-02-19 09:02:16 +08:00
FooIbar b716a98548 Resolve #789 2023-02-19 08:04:40 +08:00
FooIbar 39f40ed906 fixup! Implement #817 (#821) 2023-02-19 08:04:08 +08:00
tarsin a644a1cee1 Fixup! Move tag database downloading to FileChannel based operation 2023-02-18 13:07:26 +08:00
tarsin 61fd8dc778 Fix transfer failed msg 2023-02-18 12:55:59 +08:00
tarsin 145d5e6302 Move tag database downloading to FileChannel based operation 2023-02-18 12:18:15 +08:00
tarsin ad071e48df cleanup: Remove AssertUtils 2023-02-18 12:02:36 +08:00
tarsin 28b5717f5b Rename .java to .kt 2023-02-18 12:02:36 +08:00
tarsin 41b818caa6 cleanup: Use jsoup xml escaper 2023-02-18 11:44:55 +08:00
tarsin cf87a654fb Fixup! Prepare for 1.8.7.0 dev 2023-02-18 10:48:29 +08:00
tarsin 9ffab097bb Prepare for 1.8.7.0 dev 2023-02-18 10:43:09 +08:00
renovate[bot] bdc3c2b6b5 Update dependency gradle to v8.0.1 2023-02-18 10:35:02 +08:00
tarsin 3f39662ba1 Fix quicksearch progress 2023-02-18 10:34:47 +08:00
tarsin aa485547a6 Fix a stupid memleak 2023-02-18 01:08:56 +08:00
tarsin 747f23bfe1 Resolve #831 2023-02-18 00:20:52 +08:00
tarsin d885841826 Revert "Resolve #831"
This reverts commit 0c457ef61d.
2023-02-18 00:16:55 +08:00
tarsin 0c457ef61d Resolve #831 2023-02-18 00:09:55 +08:00
tarsin 9462f1455b Remove unused res 2023-02-17 19:22:02 +08:00
tarsin c07325e71e Cleanup ReaderPageImageView 2023-02-17 19:15:54 +08:00
tarsin 5eda7b3fc3 Remove SubsamplingScaleImageView
We have no reason to keep it, just use PhotoView
2023-02-17 19:03:32 +08:00
tarsin 6b0c208b75 Proper way to define inner singleton class 2023-02-17 18:15:53 +08:00
FooIbar b0c6a26bea Fix search keyword in tag mode 2023-02-17 18:02:51 +08:00
tarsin 67a0893a97 Fix crash of metadata not available 2023-02-17 18:02:32 +08:00
tarsin d7438a26fb Get rid of PageLoader state, convert to readyOrDie 2023-02-17 15:18:09 +08:00
tarsin ee7ab38028 Dummy coroutine operation to work ArchivePageLoader 2023-02-17 15:13:44 +08:00
tarsin 8d999ea278 Get rid of state based gallery provider 2023-02-17 15:04:25 +08:00
tarsin 0851cf50a0 Rename .java to .kt 2023-02-17 15:04:25 +08:00
tarsin 0b6676aaa3 Preheat SpiderQueen in GalleryDetailScene, Resolve #818 2023-02-17 14:38:39 +08:00
tarsin c64167c76a Merge SpiderInfo Utils 2023-02-17 14:27:11 +08:00
tarsin 72a6419aff Reorganize SpiderQueen code 2023-02-17 14:21:24 +08:00
FooIbar 64e26a6b8b Fix comment last edited time parsing 2023-02-17 14:01:57 +08:00
tarsin 23184df1b9 Coroutinize SpiderQueen 2023-02-17 14:01:29 +08:00
tarsin dd123d7b87 Rename .java to .kt 2023-02-17 14:01:29 +08:00
tarsin 0e5d1e2325 Fixup some default values 2023-02-17 12:03:22 +08:00
tarsin 4939ce37c3 Implement #817 (#821)
* Rename .java to .kt

* Initial Workmanager migration

* Fix notification

* *

* Add timepicker

* Fix TimePicker layout

* Use 12H clock

* *

* Fixup
2023-02-17 11:39:42 +08:00
tarsin 6e7e668d39 Release 1.8.6.0
Releases / Build (push) Has been cancelled
2023-02-17 11:39:05 +08:00
tarsin 2d086a5c6e Resolve #820 2023-02-17 11:36:36 +08:00
renovate[bot] f7e592eac0 Update plugin com.android.application to v8.0.0-beta03 2023-02-17 10:39:32 +08:00
121 changed files with 3323 additions and 4977 deletions

No files matched your search

+30 -12
View File
@@ -1,3 +1,8 @@
import java.io.ByteArrayOutputStream
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
plugins {
id("com.android.application")
kotlin("android")
@@ -8,7 +13,7 @@ plugins {
android {
compileSdk = 33
buildToolsVersion = "33.0.1"
buildToolsVersion = "33.0.2"
ndkVersion = "25.2.9519653"
splits {
@@ -30,12 +35,26 @@ android {
enableV4Signing = true
}
val commitSha by lazy {
val stdout = ByteArrayOutputStream()
exec {
commandLine = "git rev-parse --short=7 HEAD".split(' ')
standardOutput = stdout
}
stdout.toString().trim()
}
val buildTime by lazy {
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
formatter.format(Instant.now())
}
defaultConfig {
applicationId = "moe.tarsin.ehviewer"
minSdk = 28
targetSdk = 33
versionCode = 180031
versionName = "1.8.6.0-beta01"
versionCode = 180034
versionName = "1.8.7.0-alpha"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -52,6 +71,8 @@ android {
"nb-rNO"
)
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
externalNativeBuild {
@@ -72,15 +93,12 @@ android {
"-progressive",
"-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
@@ -125,7 +143,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.2"
kotlinCompilerExtensionVersion = "1.4.3"
}
namespace = "com.hippo.ehviewer"
@@ -144,7 +162,7 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.core:core-ktx:1.10.0-alpha02")
implementation("androidx.core:core-ktx:1.10.0-beta01")
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
@@ -169,6 +187,7 @@ dependencies {
implementation("androidx.room:room-paging:$room_version")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.work:work-runtime-ktx:2.8.0")
implementation("com.drakeet.drawer:drawer:1.0.3")
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
@@ -189,12 +208,11 @@ dependencies {
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
// https://coil-kt.github.io/coil/changelog/
// implementation 'io.coil-kt:coil:2.2.2'
implementation("com.github.Ehviewer-Overhauled.coil:coil-compose:53302f2")
implementation("io.coil-kt:coil-compose:2.2.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0-RC")
implementation("org.jsoup:jsoup:1.15.3")
implementation("org.jsoup:jsoup:1.15.4")
}
configurations.all {
+1 -1
View File
@@ -36,7 +36,7 @@
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:theme="@style/AppTheme"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".ui.MainActivity"
+1 -1
View File
@@ -45,6 +45,6 @@ SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
add_subdirectory(libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c natsort/strnatcmp.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
+95 -33
View File
@@ -17,14 +17,12 @@
* 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>
@@ -34,6 +32,7 @@
#define LOG_TAG "libarchive_wrapper"
#include "natsort/strnatcmp.h"
#include "ehviewer.h"
typedef struct {
@@ -46,12 +45,25 @@ typedef struct {
typedef struct {
const char *filename;
int index;
size_t size;
} entry;
pthread_mutex_t ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static void *mempool = NULL;
static size_t *mempoolofs = NULL;
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
#define PROT_RW (PROT_WRITE | PROT_READ)
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
@@ -59,8 +71,6 @@ 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",
@@ -87,9 +97,7 @@ static inline int filename_is_playable_file(const char *name) {
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;
return strnatcmp(fa, fb);
}
static long archive_map_entries_index(archive_ctx *ctx) {
@@ -99,6 +107,7 @@ static long archive_map_entries_index(archive_ctx *ctx) {
if (filename_is_playable_file(name)) {
entries[count].filename = strdup(name);
entries[count].index = count;
entries[count].size = archive_entry_size(ctx->entry);
count++;
}
}
@@ -108,6 +117,48 @@ static long archive_map_entries_index(archive_ctx *ctx) {
return count;
}
static bool archive_prealloc_mempool() {
mempoolofs = calloc(entryCount, sizeof(size_t));
for (int i = 0; i < entryCount; ++i) {
if (!i)
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
else
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
}
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
if (mempool == MAP_FAILED) {
LOGE("%s%s", "mmap failed with error ", strerror(errno));
return false;
}
return true;
}
// Mincore does not support anon mapping yet
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
static bool is_mempool_pages_present_and_lock(int index) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = MEMPOOL_ENTITY_SIZE(index);
mlock(addr, size);
#if 0
unsigned char vec[size / PAGE_SIZE];
unsigned char r = 1;
mincore(addr, size, vec);
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
return r;
#endif
return false;
}
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
static void mempool_release_pages(void *addr, size_t size) {
size = PAGE_ALIGN(size);
munlock(addr, size);
int ret = madvise(addr, size, MADV_FREE);
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
}
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
@@ -158,7 +209,6 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
}
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;
@@ -233,8 +283,6 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
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));
@@ -255,7 +303,7 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
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");
LOGI("%s%zu%s", "Found ", entryCount, " 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);
@@ -292,41 +340,52 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
} else {
entries = calloc(entryCount, sizeof(entry));
r = archive_map_entries_index(ctx);
if (!archive_prealloc_mempool()) {
r = 0;
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
free(entries);
}
}
archive_release_ctx(ctx);
return r;
}
#pragma clang diagnostic push
#pragma ide diagnostic ignored "ConstantConditionsOC"
#pragma ide diagnostic ignored "UnreachableCode"
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) {
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
size_t size = entries[index].size;
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret) return 0;
ret = archive_read_data(ctx->arc, addr, size);
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, addr, 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));
mempool_release_pages(addr, size);
} else {
return (*env)->NewDirectByteBuffer(env, addr, size);
}
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;
}
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
@@ -339,6 +398,8 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
munmap(mempool, MEMPOOL_SIZE);
free(mempoolofs);
for (int i = 0; i < entryCount; ++i) {
free((void *) entries[i].filename);
}
@@ -415,5 +476,6 @@ 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);
}
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
mempool_release_pages(addr, size);
}
+9
View File
@@ -1,5 +1,7 @@
#include <jni.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <android/log.h>
#include <string.h>
@@ -25,3 +27,10 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
return fd;
}
JNIEXPORT void JNICALL
Java_com_hippo_Native_sendfile(JNIEnv *env, jclass clazz, jint from, jint to) {
struct stat st;
fstat(from, &st);
sendfile(to, from, 0, st.st_size);
}
+177
View File
@@ -0,0 +1,177 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
/* partial change history:
*
* 2004-10-10 mbp: Lift out character type dependencies into macros.
*
* Eric Sosman pointed out that ctype functions take a parameter whose
* value must be that of an unsigned int, even on platforms that have
* negative chars in their default char type.
*/
#include <stddef.h> /* size_t */
#include <ctype.h>
#include "strnatcmp.h"
/* These are defined as macros to make it easier to adapt this code to
* different characters types or comparison functions. */
static inline int
nat_isdigit(nat_char a)
{
return isdigit((unsigned char) a);
}
static inline int
nat_isspace(nat_char a)
{
return isspace((unsigned char) a);
}
static inline nat_char
nat_toupper(nat_char a)
{
return toupper((unsigned char) a);
}
static int
compare_right(nat_char const *a, nat_char const *b)
{
int bias = 0;
/* The longest run of digits wins. That aside, the greatest
value wins, but we can't know that it will until we've scanned
both numbers to know that they have the same magnitude, so we
remember it in BIAS. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return bias;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b) {
if (!bias)
bias = -1;
} else if (*a > *b) {
if (!bias)
bias = +1;
} else if (!*a && !*b)
return bias;
}
return 0;
}
static int
compare_left(nat_char const *a, nat_char const *b)
{
/* Compare two left-aligned numbers: the first to have a
different value wins. */
for (;; a++, b++) {
if (!nat_isdigit(*a) && !nat_isdigit(*b))
return 0;
if (!nat_isdigit(*a))
return -1;
if (!nat_isdigit(*b))
return +1;
if (*a < *b)
return -1;
if (*a > *b)
return +1;
}
return 0;
}
static int
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
{
int ai, bi;
nat_char ca, cb;
int fractional, result;
ai = bi = 0;
while (1) {
ca = a[ai]; cb = b[bi];
/* skip over leading spaces or zeros */
while (nat_isspace(ca))
ca = a[++ai];
while (nat_isspace(cb))
cb = b[++bi];
/* process run of digits */
if (nat_isdigit(ca) && nat_isdigit(cb)) {
fractional = (ca == '0' || cb == '0');
if (fractional) {
if ((result = compare_left(a+ai, b+bi)) != 0)
return result;
} else {
if ((result = compare_right(a+ai, b+bi)) != 0)
return result;
}
}
if (!ca && !cb) {
/* The strings compare the same. Perhaps the caller
will want to call strcmp to break the tie. */
return 0;
}
if (fold_case) {
ca = nat_toupper(ca);
cb = nat_toupper(cb);
}
if (ca < cb)
return -1;
if (ca > cb)
return +1;
++ai; ++bi;
}
}
int
strnatcmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 0);
}
/* Compare, recognizing numeric string and ignoring case. */
int
strnatcasecmp(nat_char const *a, nat_char const *b) {
return strnatcmp0(a, b, 1);
}
+38
View File
@@ -0,0 +1,38 @@
/* -*- mode: c; c-file-style: "k&r" -*-
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
#ifdef __cplusplus
extern "C" {
#endif
/* CUSTOMIZATION SECTION
*
* You can change this typedef, but must then also change the inline
* functions in strnatcmp.c */
typedef char nat_char;
int strnatcmp(nat_char const *a, nat_char const *b);
int strnatcasecmp(nat_char const *a, nat_char const *b);
#ifdef __cplusplus
}
#endif
@@ -1,15 +1,8 @@
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
@@ -83,38 +76,6 @@ class ImageSource {
}
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.
*
@@ -125,16 +86,6 @@ class 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.
*
@@ -19,12 +19,9 @@ import android.view.MotionEvent
import android.view.View
import androidx.annotation.AnyThread
import androidx.annotation.IntDef
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource
import com.davemorrissey.labs.subscaleview.decoder.Decoder
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.hippo.ehviewer.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -303,42 +300,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
setMinimumDpi(160)
setDoubleTapZoomDpi(160)
setGestureDetector(context)
// Handle XML attributes
if (attr != null) {
val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView)
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {
val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName)
if (assetName != null && assetName.isNotEmpty()) {
setImage(asset(context, assetName))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {
val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0)
if (resId > 0) {
setImage(resource(context, resId))
}
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {
setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true))
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {
isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {
isQuickScaleEnabled =
typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true)
}
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {
setTileBackgroundColor(
typedAttr.getColor(
R.styleable.SubsamplingScaleImageView_tileBackgroundColor,
Color.argb(0, 0, 0, 0)
)
)
}
typedAttr.recycle()
}
quickScaleThreshold =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
}
@@ -1953,20 +1914,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
val scaleHeight = scale * sHeight
when (panLimit) {
PAN_LIMIT_CENTER -> {
vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
}
PAN_LIMIT_OUTSIDE -> {
vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F)
vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
}
else -> {
vTarget.top = -vTranslate!!.y.coerceAtLeast(0F)
vTarget.left = -vTranslate!!.x.coerceAtLeast(0F)
vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
}
@@ -4,10 +4,8 @@ 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
import com.davemorrissey.labs.subscaleview.provider.InputProvider
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
@@ -1,11 +0,0 @@
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)
}
@@ -1,10 +1,3 @@
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?
}
interface InputProvider
@@ -1,16 +0,0 @@
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
}
@@ -1,11 +0,0 @@
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)
}
@@ -1,13 +0,0 @@
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)
}
+19
View File
@@ -17,6 +17,7 @@
*/
package com.hippo
import android.os.ParcelFileDescriptor
import java.io.FileDescriptor
object Native {
@@ -26,4 +27,22 @@ object Native {
@JvmStatic
external fun getFd(fd: FileDescriptor?): Int
@JvmStatic
external fun sendfile(from: Int, to: Int)
}
val FileDescriptor.fd
get() = Native.getFd(this)
infix fun ParcelFileDescriptor.receivefrom(fd: FileDescriptor) {
Native.sendfile(fd.fd, getFd())
}
infix fun ParcelFileDescriptor.copyTo(fd: FileDescriptor) {
Native.sendfile(getFd(), fd.fd)
}
infix fun ParcelFileDescriptor.copyTo(fd: ParcelFileDescriptor) {
Native.sendfile(getFd(), fd.fd)
}
@@ -20,12 +20,14 @@ package com.hippo
import android.content.Context
import android.graphics.ImageDecoder
import android.net.Uri
import android.util.Log
import com.hippo.image.Image
import java.nio.ByteBuffer
class UriArchiveAccessor(ctx: Context, uri: Uri) {
class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
fun open(): Int {
Log.d(DEBUG_TAG, "Open archive $uri")
return openArchive(pfd.fd, pfd.statSize)
}
@@ -39,6 +41,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
fun close() {
closeArchive()
pfd.close()
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
}
fun getImageSource(index: Int): Image.CloseableSource? {
@@ -61,4 +64,6 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
@JvmStatic
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
}
private const val DEBUG_TAG = "UriArchiveAccessor"
@@ -1,190 +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.drawable;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
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;
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) {
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) {
mBounds = bounds;
}
public int getChangingConfigurations() {
return this.mDrawable.getChangingConfigurations();
}
public void setChangingConfigurations(int configs) {
this.mDrawable.setChangingConfigurations(configs);
}
public void setDither(boolean dither) {
this.mDrawable.setDither(dither);
}
public void setFilterBitmap(boolean filter) {
this.mDrawable.setFilterBitmap(filter);
}
public void setAlpha(int alpha) {
this.mDrawable.setAlpha(alpha);
}
public void setColorFilter(ColorFilter cf) {
this.mDrawable.setColorFilter(cf);
}
public boolean isStateful() {
return this.mDrawable.isStateful();
}
public boolean setState(@NonNull int[] stateSet) {
return this.mDrawable.setState(stateSet);
}
@NonNull
public int[] getState() {
return this.mDrawable.getState();
}
public void jumpToCurrentState() {
this.mDrawable.jumpToCurrentState();
}
@NonNull
public Drawable getCurrent() {
return this.mDrawable.getCurrent();
}
public boolean setVisible(boolean visible, boolean restart) {
return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart);
}
public int getOpacity() {
return this.mDrawable.getOpacity();
}
public Region getTransparentRegion() {
return this.mDrawable.getTransparentRegion();
}
public int getIntrinsicWidth() {
return this.mDrawable.getIntrinsicWidth();
}
public int getIntrinsicHeight() {
return this.mDrawable.getIntrinsicHeight();
}
public int getMinimumWidth() {
return this.mDrawable.getMinimumWidth();
}
public int getMinimumHeight() {
return this.mDrawable.getMinimumHeight();
}
public boolean getPadding(@NonNull Rect padding) {
return this.mDrawable.getPadding(padding);
}
public void invalidateDrawable(@NonNull Drawable who) {
this.invalidateSelf();
}
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
this.scheduleSelf(what, when);
}
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
this.unscheduleSelf(what);
}
protected boolean onLevelChange(int level) {
return this.mDrawable.setLevel(level);
}
public boolean isAutoMirrored() {
return DrawableCompat.isAutoMirrored(this.mDrawable);
}
public void setAutoMirrored(boolean mirrored) {
DrawableCompat.setAutoMirrored(this.mDrawable, mirrored);
}
public void setTint(int tint) {
DrawableCompat.setTint(this.mDrawable, tint);
}
public void setTintList(ColorStateList tint) {
DrawableCompat.setTintList(this.mDrawable, tint);
}
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
DrawableCompat.setTintMode(this.mDrawable, tintMode);
}
public void setHotspot(float x, float y) {
DrawableCompat.setHotspot(this.mDrawable, x, y);
}
public void setHotspotBounds(int left, int top, int right, int bottom) {
DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom);
}
public Drawable getWrappedDrawable() {
return this.mDrawable;
}
public void setWrappedDrawable(Drawable drawable) {
if (this.mDrawable != null) {
this.mDrawable.setCallback(null);
}
this.mDrawable = drawable;
if (drawable != null) {
drawable.setCallback(this);
}
}
}
@@ -1,104 +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.drawable;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import com.hippo.yorozuya.MathUtils;
/**
* Show a part of the original drawable
*/
public class PreciselyClipDrawable extends DrawableWrapper {
private final boolean mClip;
private RectF mScale;
private Rect mTemp;
public PreciselyClipDrawable(Drawable drawable, int offsetX, int offsetY, int width, int height) {
super(drawable);
float originWidth = drawable.getIntrinsicWidth();
float originHeight = drawable.getIntrinsicHeight();
if (originWidth <= 0 || originHeight <= 0) {
// Can not clip
mClip = false;
} else {
mClip = true;
mScale = new RectF();
mScale.set(MathUtils.clamp(offsetX / originWidth, 0.0f, 1.0f),
MathUtils.clamp(offsetY / originHeight, 0.0f, 1.0f),
MathUtils.clamp((offsetX + width) / originWidth, 0.0f, 1.0f),
MathUtils.clamp((offsetY + height) / originHeight, 0.0f, 1.0f));
mTemp = new Rect();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
if (mClip) {
if (!mScale.isEmpty()) {
mTemp.left = (int) ((mScale.left * bounds.right - mScale.right * bounds.left) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.right = (int) (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) /
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
mTemp.top = (int) ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
mTemp.bottom = (int) (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) /
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
super.onBoundsChange(mTemp);
}
} else {
super.onBoundsChange(bounds);
}
}
@Override
public int getIntrinsicWidth() {
if (mClip) {
return (int) (super.getIntrinsicWidth() * mScale.width());
} else {
return super.getIntrinsicWidth();
}
}
@Override
public int getIntrinsicHeight() {
if (mClip) {
return (int) (super.getIntrinsicHeight() * mScale.height());
} else {
return super.getIntrinsicHeight();
}
}
@Override
public void draw(Canvas canvas) {
if (mClip) {
if (!mScale.isEmpty()) {
int saveCount = canvas.save();
canvas.clipRect(getBounds());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
} else {
super.draw(canvas);
}
}
}
@@ -0,0 +1,68 @@
/*
* 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.drawable
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableWrapper
import androidx.core.graphics.withClip
/**
* Show a part of the original drawable
*/
class PreciselyClipDrawable(
drawable: Drawable,
offsetX: Int,
offsetY: Int,
width: Int,
height: Int
) : DrawableWrapper(drawable) {
private val mScale: RectF
private val mTemp = Rect()
init {
val originWidth = drawable.intrinsicWidth.toFloat()
val originHeight = drawable.intrinsicHeight.toFloat()
mScale = RectF(
(offsetX / originWidth).coerceIn(0.0f, 1.0f),
(offsetY / originHeight).coerceIn(0.0f, 1.0f),
((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),
((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),
)
}
override fun onBoundsChange(bounds: Rect) {
mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
super.onBoundsChange(mTemp)
}
override fun getIntrinsicWidth(): Int {
return (super.getIntrinsicWidth() * mScale.width()).toInt()
}
override fun getIntrinsicHeight(): Int {
return (super.getIntrinsicHeight() * mScale.height()).toInt()
}
override fun draw(canvas: Canvas) {
canvas.withClip(bounds) { super.draw(canvas) }
}
}
@@ -1,202 +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;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Debug;
import androidx.annotation.NonNull;
import com.hippo.util.PackageUtils;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.OSUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
public final class Crash {
private Crash() {
}
@NonNull
private static String avoidNull(String str) {
return null != str ? str : "null";
}
private static void collectInfo(Context context, FileWriter fw) throws IOException {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = String.valueOf(pi.versionCode);
fw.write("======== PackageInfo ========\r\n");
fw.write("PackageName=");
fw.write(pi.packageName);
fw.write("\r\n");
fw.write("VersionName=");
fw.write(versionName);
fw.write("\r\n");
fw.write("VersionCode=");
fw.write(versionCode);
fw.write("\r\n");
String signature = PackageUtils.getSignature(context, pi.packageName);
fw.write("Signature=");
fw.write(null != signature ? signature : "null");
fw.write("\r\n");
fw.write("\r\n");
}
} catch (PackageManager.NameNotFoundException e) {
fw.write("======== PackageInfo ========\r\n");
fw.write("Can't get package information\r\n");
fw.write("\r\n");
}
// Runtime
String topActivityClazzName = "null";
fw.write("======== Runtime ========\r\n");
fw.write("TopActivity=");
fw.write(avoidNull(topActivityClazzName));
fw.write("\r\n");
fw.write("\r\n");
fw.write("\r\n");
// Device info
fw.write("======== DeviceInfo ========\r\n");
fw.write("BOARD=");
fw.write(Build.BOARD);
fw.write("\r\n");
fw.write("BOOTLOADER=");
fw.write(Build.BOOTLOADER);
fw.write("\r\n");
fw.write("SUPPORTED_ABIS=");
fw.write(Arrays.toString(Build.SUPPORTED_ABIS));
fw.write("\r\n");
fw.write("DEVICE=");
fw.write(Build.DEVICE);
fw.write("\r\n");
fw.write("DISPLAY=");
fw.write(Build.DISPLAY);
fw.write("\r\n");
fw.write("FINGERPRINT=");
fw.write(Build.FINGERPRINT);
fw.write("\r\n");
fw.write("HARDWARE=");
fw.write(Build.HARDWARE);
fw.write("\r\n");
fw.write("HOST=");
fw.write(Build.HOST);
fw.write("\r\n");
fw.write("ID=");
fw.write(Build.ID);
fw.write("\r\n");
fw.write("MANUFACTURER=");
fw.write(Build.MANUFACTURER);
fw.write("\r\n");
fw.write("MODEL=");
fw.write(Build.MODEL);
fw.write("\r\n");
fw.write("PRODUCT=");
fw.write(Build.PRODUCT);
fw.write("\r\n");
fw.write("RADIO=");
fw.write(Build.getRadioVersion());
fw.write("\r\n");
fw.write("TAGS=");
fw.write(Build.TAGS);
fw.write("\r\n");
fw.write("TYPE=");
fw.write(Build.TYPE);
fw.write("\r\n");
fw.write("USER=");
fw.write(Build.USER);
fw.write("\r\n");
fw.write("CODENAME=");
fw.write(Build.VERSION.CODENAME);
fw.write("\r\n");
fw.write("INCREMENTAL=");
fw.write(Build.VERSION.INCREMENTAL);
fw.write("\r\n");
fw.write("RELEASE=");
fw.write(Build.VERSION.RELEASE);
fw.write("\r\n");
fw.write("SDK=");
fw.write(Integer.toString(Build.VERSION.SDK_INT));
fw.write("\r\n");
fw.write("MEMORY=");
fw.write(
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_NATIVE=");
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
fw.write("\r\n");
fw.write("MEMORY_MAX=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));
fw.write("\r\n");
fw.write("MEMORY_TOTAL=");
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));
fw.write("\r\n");
fw.write("\r\n");
}
private static void getThrowableInfo(Throwable t, FileWriter fw) {
PrintWriter printWriter = new PrintWriter(fw);
t.printStackTrace(printWriter);
Throwable cause = t.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
}
public static void saveCrashLog(Context context, Throwable t) {
File dir = AppConfig.getExternalCrashDir();
if (dir == null) {
return;
}
String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
String fileName = "crash-" + nowString + ".log";
File file = new File(dir, fileName);
FileWriter fw = null;
try {
fw = new FileWriter(file);
fw.write("TIME=");
fw.write(nowString);
fw.write("\r\n");
fw.write("\r\n");
collectInfo(context, fw);
fw.write("======== CrashInfo ========\r\n");
getThrowableInfo(t, fw);
fw.write("\r\n");
fw.flush();
} catch (Exception e) {
file.delete();
} finally {
IOUtils.closeQuietly(fw);
}
}
}
@@ -0,0 +1,173 @@
/*
* 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
import android.os.Build
import android.os.Debug
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import com.hippo.yorozuya.OSUtils
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.io.PrintWriter
import java.util.Arrays
object Crash {
@Throws(IOException::class)
private fun collectInfo(fw: FileWriter) {
fw.write("======== PackageInfo ========\r\n")
fw.write("PackageName=")
fw.write(BuildConfig.APPLICATION_ID)
fw.write("\r\n")
fw.write("VersionName=")
fw.write(BuildConfig.VERSION_NAME)
fw.write("\r\n")
fw.write("VersionCode=")
fw.write(BuildConfig.VERSION_CODE)
fw.write("\r\n")
fw.write("CommitSha=")
fw.write(BuildConfig.COMMIT_SHA)
fw.write("\r\n")
fw.write("BuildTime=")
fw.write(BuildConfig.BUILD_TIME)
fw.write("\r\n")
fw.write("\r\n")
// Runtime
val topActivityClazzName = EhApplication.application.topActivity?.javaClass?.name
fw.write("======== Runtime ========\r\n")
fw.write("TopActivity=")
fw.write(topActivityClazzName ?: "null")
fw.write("\r\n")
fw.write("\r\n")
fw.write("\r\n")
// Device info
fw.write("======== DeviceInfo ========\r\n")
fw.write("BOARD=")
fw.write(Build.BOARD)
fw.write("\r\n")
fw.write("BOOTLOADER=")
fw.write(Build.BOOTLOADER)
fw.write("\r\n")
fw.write("SUPPORTED_ABIS=")
fw.write(Arrays.toString(Build.SUPPORTED_ABIS))
fw.write("\r\n")
fw.write("DEVICE=")
fw.write(Build.DEVICE)
fw.write("\r\n")
fw.write("DISPLAY=")
fw.write(Build.DISPLAY)
fw.write("\r\n")
fw.write("FINGERPRINT=")
fw.write(Build.FINGERPRINT)
fw.write("\r\n")
fw.write("HARDWARE=")
fw.write(Build.HARDWARE)
fw.write("\r\n")
fw.write("HOST=")
fw.write(Build.HOST)
fw.write("\r\n")
fw.write("ID=")
fw.write(Build.ID)
fw.write("\r\n")
fw.write("MANUFACTURER=")
fw.write(Build.MANUFACTURER)
fw.write("\r\n")
fw.write("MODEL=")
fw.write(Build.MODEL)
fw.write("\r\n")
fw.write("PRODUCT=")
fw.write(Build.PRODUCT)
fw.write("\r\n")
fw.write("RADIO=")
fw.write(Build.getRadioVersion())
fw.write("\r\n")
fw.write("TAGS=")
fw.write(Build.TAGS)
fw.write("\r\n")
fw.write("TYPE=")
fw.write(Build.TYPE)
fw.write("\r\n")
fw.write("USER=")
fw.write(Build.USER)
fw.write("\r\n")
fw.write("CODENAME=")
fw.write(Build.VERSION.CODENAME)
fw.write("\r\n")
fw.write("INCREMENTAL=")
fw.write(Build.VERSION.INCREMENTAL)
fw.write("\r\n")
fw.write("RELEASE=")
fw.write(Build.VERSION.RELEASE)
fw.write("\r\n")
fw.write("SDK=")
fw.write(Build.VERSION.SDK_INT.toString())
fw.write("\r\n")
fw.write("MEMORY=")
fw.write(
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false)
)
fw.write("\r\n")
fw.write("MEMORY_NATIVE=")
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
fw.write("\r\n")
fw.write("MEMORY_MAX=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
fw.write("\r\n")
fw.write("MEMORY_TOTAL=")
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
fw.write("\r\n")
fw.write("\r\n")
}
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
val printWriter = PrintWriter(fw)
t.printStackTrace(printWriter)
var cause = t.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
}
fun saveCrashLog(t: Throwable) {
val dir = AppConfig.getExternalCrashDir() ?: return
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
val fileName = "crash-$nowString.log"
val file = File(dir, fileName)
var fw: FileWriter? = null
try {
fw = FileWriter(file)
fw.write("TIME=")
fw.write(nowString)
fw.write("\r\n")
fw.write("\r\n")
collectInfo(fw)
fw.write("======== CrashInfo ========\r\n")
getThrowableInfo(t, fw)
fw.write("\r\n")
fw.flush()
} catch (e: Exception) {
file.delete()
} finally {
IOUtils.closeQuietly(fw)
}
}
}
@@ -18,31 +18,22 @@ 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 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.data.GalleryDetail
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.CommonOperations
@@ -55,8 +46,6 @@ 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.OkHttpClient
import java.net.Proxy
import java.security.KeyStore
@@ -87,7 +76,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(application, e)
Crash.saveCrashLog(e)
}
} catch (ignored: Throwable) {
}
@@ -106,14 +95,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
ehDatabase
}
launchIO {
downloadManager
DownloadManager.isIdle
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
}
@@ -140,54 +126,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
private fun theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return
}
if (ehCookieStore.hasSignedIn()) {
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) {
@@ -243,16 +181,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
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))
crossfade(true)
components { add(MergeInterceptor) }
crossfade(300)
}.build()
}
@@ -300,9 +233,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
@JvmStatic
val downloadManager by lazy { DownloadManager }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@@ -1,591 +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.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
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.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.HistoryDao;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
import com.hippo.ehviewer.dao.LocalFavoritesDao;
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.yorozuya.IOUtils;
import com.hippo.yorozuya.ObjectUtils;
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.util.ArrayList;
import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static final EhDatabase db = EhApplication.getEhDatabase();
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
case 1: // 1 to 2, add FILTER
db.execSQL("CREATE TABLE IF NOT EXISTS \"FILTER\" (" + //
"\"_id\" INTEGER PRIMARY KEY ," + // 0: id
"\"MODE\" INTEGER NOT NULL ," + // 1: mode
"\"TEXT\" TEXT," + // 2: text
"\"ENABLE\" INTEGER);"); // 3: enable
case 2: // 2 to 3, add ENABLE column to table FILTER
db.execSQL("CREATE TABLE " + "\"FILTER2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"MODE\" INTEGER NOT NULL ," +
"\"TEXT\" TEXT," +
"\"ENABLE\" INTEGER);");
db.execSQL("INSERT INTO \"FILTER2\" (" +
"_id, MODE, TEXT, ENABLE)" +
"SELECT _id, MODE, TEXT, 1 FROM FILTER;");
db.execSQL("DROP TABLE FILTER");
db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER");
case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH
db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" +
"\"_id\" INTEGER PRIMARY KEY ," +
"\"NAME\" TEXT," +
"\"MODE\" INTEGER NOT NULL ," +
"\"CATEGORY\" INTEGER NOT NULL ," +
"\"KEYWORD\" TEXT," +
"\"ADVANCE_SEARCH\" INTEGER NOT NULL ," +
"\"MIN_RATING\" INTEGER NOT NULL ," +
"\"PAGE_FROM\" INTEGER NOT NULL ," +
"\"PAGE_TO\" INTEGER NOT NULL ," +
"\"TIME\" INTEGER NOT NULL );");
db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" +
"_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" +
"SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;");
db.execSQL("DROP TABLE QUICK_SEARCH");
db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH");
}
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
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) {
info.state = DownloadInfo.STATE_NONE;
}
}
return list;
}
public static synchronized void updateDownloadInfo(List<DownloadInfo> downloadInfos) {
DownloadsDao dao = db.downloadsDao();
dao.update(downloadInfos);
}
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = db.downloadsDao();
if (null != dao.load(downloadInfo.getGid())) {
// Update
dao.update(downloadInfo);
} else {
// Insert
dao.insert(downloadInfo);
}
}
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
db.downloadsDao().delete(downloadInfo);
}
@Nullable
public static synchronized String getDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) {
return raw.getDirname();
} else {
return null;
}
}
/**
* Insert or update
*/
public static synchronized void putDownloadDirname(long gid, String dirname) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) { // Update
raw.setDirname(dirname);
dao.update(raw);
} else { // Insert
raw = new DownloadDirname();
raw.setGid(gid);
raw.setDirname(dirname);
dao.insert(raw);
}
}
public static synchronized void removeDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteByKey(gid);
}
public static synchronized void clearDownloadDirname() {
DownloadDirnameDao dao = db.downloadDirnameDao();
dao.deleteAll();
}
@NonNull
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
DownloadLabelDao dao = db.downloadLabelDao();
return dao.list();
}
public static synchronized DownloadLabel addDownloadLabel(String label) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabel raw = new DownloadLabel();
raw.setLabel(label);
raw.setTime(System.currentTimeMillis());
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
// Reset id
raw.setId(null);
DownloadLabelDao dao = db.downloadLabelDao();
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.update(raw);
}
public static synchronized void moveDownloadLabel(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;
DownloadLabelDao dao = db.downloadLabelDao();
List<DownloadLabel> 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).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
dao.delete(raw);
}
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
}
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
return new ArrayList<>(list);
}
public static synchronized void removeLocalFavorites(long gid) {
db.localFavoritesDao().deleteByKey(gid);
}
public static synchronized void removeLocalFavorites(long[] gidArray) {
LocalFavoritesDao dao = db.localFavoritesDao();
for (long gid : gidArray) {
dao.deleteByKey(gid);
}
}
public static synchronized boolean containLocalFavorites(long gid) {
LocalFavoritesDao dao = db.localFavoritesDao();
return null != dao.load(gid);
}
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = db.localFavoritesDao();
if (null == dao.load(galleryInfo.getGid())) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
info = (LocalFavoriteInfo) galleryInfo;
} else {
info = new LocalFavoriteInfo(galleryInfo);
info.time = System.currentTimeMillis();
}
dao.insert(info);
}
}
public static synchronized void putLocalFavorites(List<GalleryInfo> galleryInfoList) {
for (GalleryInfo gi : galleryInfoList) {
putLocalFavorites(gi);
}
}
public static synchronized List<QuickSearch> getAllQuickSearch() {
QuickSearchDao dao = db.quickSearchDao();
return dao.list();
}
public static synchronized void insertQuickSearch(QuickSearch 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 = db.quickSearchDao();
for (QuickSearch quickSearch : quickSearchList) {
dao.insert(quickSearch);
}
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
dao.delete(quickSearch);
}
public static synchronized void moveQuickSearch(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;
QuickSearchDao dao = db.quickSearchDao();
List<QuickSearch> 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).getTime();
for (int i = end; reverse ? i < start : i > start; i += step) {
list.get(i).setTime(list.get(i + step).getTime());
}
list.get(start).setTime(toTime);
dao.update(list);
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
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 {
dao.insert(info);
}
}
public static synchronized void putHistoryInfoNonRefresh(GalleryInfo info) {
HistoryDao dao = db.historyDao();
var i = dao.load(info.getGid());
if (null != i) {
HistoryInfo historyInfo;
if (info instanceof HistoryInfo) {
historyInfo = (HistoryInfo) info;
} else {
historyInfo = new HistoryInfo(info);
historyInfo.time = i.time;
}
dao.update(historyInfo);
}
}
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = db.historyDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.getGid())) {
dao.insert(info);
}
}
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
HistoryDao dao = db.historyDao();
dao.delete(info);
}
public static synchronized void clearHistoryInfo() {
HistoryDao dao = db.historyDao();
dao.deleteAll();
}
public static synchronized List<Filter> getAllFilter() {
return db.filterDao().list();
}
public static synchronized boolean addFilter(Filter filter) {
Filter existFilter;
try {
existFilter = db.filterDao().load(filter.text, filter.mode);
} catch (Exception e) {
existFilter = null;
}
if (existFilter == null) {
filter.setId(null);
filter.setId(db.filterDao().insert(filter));
return true;
} else {
return false;
}
}
public static synchronized void deleteFilter(Filter filter) {
db.filterDao().delete(filter);
}
public static synchronized void triggerFilter(Filter filter) {
filter.enable = !filter.enable;
db.filterDao().update(filter);
}
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) {
final String ehExportName = "eh.export.db";
// Delete old export db
context.deleteDatabase(ehExportName);
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
try {
// Copy data to a export db
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
File dbFile = context.getDatabasePath(ehExportName);
if (dbFile == null || !dbFile.isFile()) {
return false;
}
InputStream is = null;
OutputStream os = null;
try {
is = new FileInputStream(dbFile);
os = context.getContentResolver().openOutputStream(uri);
IOUtils.copy(is, os);
return true;
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(os);
}
// Delete failed file
return false;
} finally {
context.deleteDatabase(ehExportName);
}
}
/**
* @return error string, null for no error
*/
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];
int read;
if (inputStream != null) {
while ((read = inputStream.read(buff, 0, buff.length)) > 0) {
outputStream.write(buff, 0, read);
}
} else {
return context.getString(R.string.cant_read_the_file);
}
inputStream.close();
outputStream.close();
// Check database version
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
int newVersion = CUR_DB_VER;
int oldVersion = oldDB.getVersion();
if (oldVersion < newVersion) {
upgradeDB(oldDB, oldVersion);
oldDB.setVersion(newVersion);
} else if (oldVersion > newVersion) {
return context.getString(R.string.db_version_too_high);
}
// 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
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
try {
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
} catch (Exception ignored) {
}
// History
try {
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
putHistoryInfo(historyInfoList);
} catch (Exception ignored) {
}
// QuickSearch
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);
}
importQuickSearch(importList);
} catch (Exception ignored) {
}
// LocalFavorites
try {
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
} catch (Exception ignored) {
}
// 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);
// Ignore
return context.getString(R.string.cant_read_the_file);
}
}
}
@@ -0,0 +1,459 @@
/*
* 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.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import androidx.paging.PagingSource
import androidx.room.Room.databaseBuilder
import com.hippo.Native
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.BasicDao
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.dao.EhDatabase
import com.hippo.ehviewer.dao.Filter
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.dao.LocalFavoriteInfo
import com.hippo.ehviewer.dao.QuickSearch
import com.hippo.ehviewer.download.DownloadManager
object EhDB {
private const val CUR_DB_VER = 4
private val db = ehDatabase
// Fix state
@get:Synchronized
val allDownloadInfo: List<DownloadInfo>
get() = db.downloadsDao().list().onEach {
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
it.state = DownloadInfo.STATE_NONE
}
}
@Synchronized
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
val dao = db.downloadsDao()
dao.update(downloadInfos)
}
@Synchronized
fun putDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().run {
if (load(downloadInfo.gid) != null) {
update(downloadInfo)
} else {
insert(downloadInfo)
}
}
}
@Synchronized
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
db.downloadsDao().delete(downloadInfo)
}
@Synchronized
fun getDownloadDirname(gid: Long): String? {
val dao = db.downloadDirnameDao()
val raw = dao.load(gid)
return raw?.dirname
}
@Synchronized
fun putDownloadDirname(gid: Long, dirname: String?) {
val dao = db.downloadDirnameDao()
var raw = dao.load(gid)
if (raw != null) {
raw.dirname = dirname
dao.update(raw)
} else {
raw = DownloadDirname(gid, dirname)
dao.insert(raw)
}
}
@Synchronized
fun removeDownloadDirname(gid: Long) {
val dao = db.downloadDirnameDao()
dao.deleteByKey(gid)
}
@Synchronized
fun clearDownloadDirname() {
val dao = db.downloadDirnameDao()
dao.deleteAll()
}
@get:Synchronized
val allDownloadLabelList: List<DownloadLabel>
get() = db.downloadLabelDao().list()
@Synchronized
fun addDownloadLabel(label: String): DownloadLabel {
val dao = db.downloadLabelDao()
val raw = DownloadLabel()
raw.label = label
raw.time = System.currentTimeMillis()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
// Reset id
raw.id = null
val dao = db.downloadLabelDao()
raw.id = dao.insert(raw)
return raw
}
@Synchronized
fun updateDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.update(raw!!)
}
@Synchronized
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.downloadLabelDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@Synchronized
fun removeDownloadLabel(raw: DownloadLabel?) {
val dao = db.downloadLabelDao()
dao.delete(raw!!)
}
@get:Synchronized
val allLocalFavorites: List<GalleryInfo>
get() {
val dao = db.localFavoritesDao()
val list = dao.list()
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun searchLocalFavorites(query: String): List<GalleryInfo> {
val dao = db.localFavoritesDao()
val list = dao.list("%$query%")
return ArrayList<GalleryInfo>(list)
}
@Synchronized
fun removeLocalFavorites(gid: Long) {
db.localFavoritesDao().deleteByKey(gid)
}
@Synchronized
fun removeLocalFavorites(gidArray: LongArray) {
val dao = db.localFavoritesDao()
for (gid in gidArray) {
dao.deleteByKey(gid)
}
}
@JvmStatic
@Synchronized
fun containLocalFavorites(gid: Long): Boolean {
val dao = db.localFavoritesDao()
return null != dao.load(gid)
}
@Synchronized
fun putLocalFavorites(galleryInfo: GalleryInfo) {
val dao = db.localFavoritesDao()
if (null == dao.load(galleryInfo.gid)) {
val info: LocalFavoriteInfo
if (galleryInfo is LocalFavoriteInfo) {
info = galleryInfo
} else {
info = LocalFavoriteInfo(galleryInfo)
info.time = System.currentTimeMillis()
}
dao.insert(info)
}
}
@Synchronized
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
for (gi in galleryInfoList) {
putLocalFavorites(gi)
}
}
@get:Synchronized
val allQuickSearch: List<QuickSearch>
get() {
val dao = db.quickSearchDao()
return dao.list()
}
@Synchronized
fun insertQuickSearch(quickSearch: QuickSearch) {
val dao = db.quickSearchDao()
quickSearch.id = null
quickSearch.time = System.currentTimeMillis()
quickSearch.id = dao.insert(quickSearch)
}
@Synchronized
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
val dao = db.quickSearchDao()
for (quickSearch in quickSearchList) {
dao.insert(quickSearch!!)
}
}
@Synchronized
fun deleteQuickSearch(quickSearch: QuickSearch?) {
val dao = db.quickSearchDao()
dao.delete(quickSearch)
}
@Synchronized
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) {
return
}
val reverse = fromPosition > toPosition
val offset = if (reverse) toPosition else fromPosition
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
val dao = db.quickSearchDao()
val list = dao.list(offset, limit)
val step = if (reverse) 1 else -1
val start = if (reverse) limit - 1 else 0
val end = if (reverse) 0 else limit - 1
val toTime = list[end].time
var i = end
while (if (reverse) i < start else i > start) {
list[i].time = list[i + step].time
i += step
}
list[start].time = toTime
dao.update(list)
}
@get:Synchronized
val historyLazyList: PagingSource<Int, HistoryInfo>
get() = db.historyDao().listLazy()
@Synchronized
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
val dao = db.historyDao()
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
galleryInfo
} else {
HistoryInfo(galleryInfo!!)
}
info.time = System.currentTimeMillis()
if (null != dao.load(info.gid)) {
dao.update(info)
} else {
dao.insert(info)
}
}
@Synchronized
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
val dao = db.historyDao()
val i = dao.load(info.gid)
if (null != i) {
val historyInfo: HistoryInfo
if (info is HistoryInfo) {
historyInfo = info
} else {
historyInfo = HistoryInfo(info)
historyInfo.time = i.time
}
dao.update(historyInfo)
}
}
@Synchronized
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
val dao = db.historyDao()
for (info in historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info)
}
}
}
@Synchronized
fun deleteHistoryInfo(info: HistoryInfo?) {
val dao = db.historyDao()
dao.delete(info!!)
}
@Synchronized
fun clearHistoryInfo() {
val dao = db.historyDao()
dao.deleteAll()
}
@get:Synchronized
val allFilter: List<Filter>
get() = db.filterDao().list()
@Synchronized
fun addFilter(filter: Filter): Boolean {
val existFilter: Filter? = try {
db.filterDao().load(filter.text!!, filter.mode)
} catch (e: Exception) {
null
}
return if (existFilter == null) {
filter.id = null
filter.id = db.filterDao().insert(filter)
true
} else {
false
}
}
@Synchronized
fun deleteFilter(filter: Filter) {
db.filterDao().delete(filter)
}
@Synchronized
fun triggerFilter(filter: Filter) {
filter.enable = filter.enable?.not() ?: false
db.filterDao().update(filter)
}
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
val list = from.list()
for (item in list) to.insert(item)
}
@Synchronized
fun exportDB(context: Context, uri: Uri): Boolean {
val ehExportName = "eh.export.db"
runCatching {
context.deleteDatabase(ehExportName)
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
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())
val dbFile = context.getDatabasePath(ehExportName)
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
Native.sendfile(fromFd.fd, toFd.fd)
}
}
return true
}.onFailure {
it.printStackTrace()
}
return false
}
/**
* @return error string, null for no error
*/
@Synchronized
fun importDB(context: Context, uri: Uri): String? {
runCatching {
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
.createFromInputStream { context.contentResolver.openInputStream(uri) }.build()
// Download label
val manager = DownloadManager
runCatching {
val downloadLabelList = oldDB.downloadLabelDao().list()
manager.addDownloadLabel(downloadLabelList)
}
// Downloads
runCatching {
val downloadInfoList = oldDB.downloadsDao().list()
manager.addDownload(downloadInfoList, false)
}
// Download dirname
runCatching {
oldDB.downloadDirnameDao().list().forEach {
putDownloadDirname(it.gid, it.dirname)
}
}
// History
runCatching {
val historyInfoList = oldDB.historyDao().list()
putHistoryInfo(historyInfoList)
}
// QuickSearch
runCatching {
val quickSearchList = oldDB.quickSearchDao().list()
val currentQuickSearchList = db.quickSearchDao().list()
val importList = quickSearchList.mapNotNull { newQS ->
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
}
importQuickSearch(importList)
}
// LocalFavorites
runCatching {
oldDB.localFavoritesDao().list().forEach {
putLocalFavorites(it)
}
}
// Filter
runCatching {
val filterList = oldDB.filterDao().list()
val currentFilterList = db.filterDao().list()
filterList.forEach {
if (it !in currentFilterList) addFilter(it)
}
}
oldDB.close()
}.onFailure {
it.printStackTrace()
return context.getString(R.string.cant_read_the_file)
}
return null
}
}
@@ -30,7 +30,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.AssertUtilsKt;
import com.hippo.yorozuya.NumberUtils;
import java.util.Locale;
@@ -63,6 +63,9 @@ public class Settings {
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
public static final String KEY_REQUEST_NEWS = "request_news";
public static final String KEY_REQUEST_NEWS_TIMER = "request_news_timer";
public static final String KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour";
public static final String KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute";
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
public static final String[] SIGN_IN_REQUIRED = {
KEY_IMAGE_LIMITS,
@@ -71,6 +74,7 @@ public class Settings {
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_REQUEST_NEWS_TIMER,
KEY_HIDE_HV_EVENTS
};
/********************
@@ -103,7 +107,7 @@ public class Settings {
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_BYPASS_VPN = "bypass_vpn";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
private static final boolean DEFAULT_HIDE_HV_EVENTS = false;
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();
@@ -205,7 +209,7 @@ public class Settings {
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 boolean DEFAULT_REQUEST_NEWS = true;
private static final boolean DEFAULT_REQUEST_NEWS = false;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
@@ -562,7 +566,7 @@ public class Settings {
}
public static void putFavCat(String[] value) {
AssertUtils.assertEquals(10, value.length);
AssertUtilsKt.assertEquals(10, value.length);
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
@@ -593,7 +597,7 @@ public class Settings {
}
public static void putFavCount(int[] count) {
AssertUtils.assertEquals(10, count.length);
AssertUtilsKt.assertEquals(10, count.length);
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
@@ -183,7 +183,10 @@ object EhCookieStore : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
addCookie(cookie)
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
}
}
@@ -20,7 +20,6 @@ import android.util.Log
import android.util.Pair
import com.hippo.ehviewer.AppConfig
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -50,7 +49,6 @@ import com.hippo.ehviewer.client.parser.VoteCommentParser
import com.hippo.ehviewer.client.parser.VoteTagParser
import com.hippo.network.StatusCodeException
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.AssertUtils
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
@@ -322,10 +320,6 @@ object EhEngine {
code = response.code
headers = response.headers
body = response.body.string()
val html = EventPaneParser.parse(body)
if (html != null) {
application.showEventPane(html)
}
return GalleryDetailParser.parse(body!!)
}
} catch (e: Throwable) {
@@ -566,7 +560,7 @@ object EhEngine {
gidArray: LongArray,
tokenArray: Array<String?>, dstCat: Int
): Void? {
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
check(gidArray.size == tokenArray.size)
var i = 0
val n = gidArray.size
while (i < n) {
@@ -847,6 +841,30 @@ object EhEngine {
}
}
@Throws(Throwable::class)
suspend fun getNews(parse: Boolean): String? {
val url = EhUrl.URL_NEWS
val referer = EhUrl.REFERER_E
Log.d(TAG, url)
val request = EhRequestBuilder(url, referer).build()
val call = okHttpClient.newCall(request)
var body: String? = null
var headers: Headers? = null
var code = -1
try {
call.executeAsync().use { response ->
code = response.code
headers = response.headers
body = response.body.string()
return if (parse) EventPaneParser.parse(body!!) else null
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
transformException(code, headers, body, e)
throw e
}
}
@Throws(Throwable::class)
private suspend fun getProfileInternal(
url: String, referer: String
@@ -39,7 +39,7 @@ object EhFilter {
private val TAG = EhFilter::class.java.simpleName
init {
val list = EhDB.getAllFilter()
val list = EhDB.allFilter
var i = 0
val n = list.size
while (i < n) {
@@ -112,7 +112,7 @@ object EhFilter {
}
@Synchronized
fun triggerFilter(filter: Filter?) {
fun triggerFilter(filter: Filter) {
EhDB.triggerFilter(filter)
}
@@ -20,10 +20,9 @@ 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 com.hippo.yorozuya.copyToFile
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -37,7 +36,6 @@ 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
@@ -197,22 +195,21 @@ object EhTagDatabase {
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
val request: Request = Request.Builder().url(url).build()
val call = client.newCall(request)
try {
runCatching {
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) } }
response.body.use {
it.copyToFile(file)
}
return true
}
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}.onFailure {
file.delete()
it.printStackTrace()
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
@@ -46,6 +46,7 @@ object EhUrl {
const val URL_FORUMS = "https://forums.e-hentai.org/"
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
const val URL_HOME = HOST_E + "home.php"
const val URL_NEWS = HOST_E + "news.php"
const val REFERER_EX = "https://$DOMAIN_EX"
const val ORIGIN_EX = REFERER_EX
const val REFERER_E = "https://$DOMAIN_E"
@@ -15,31 +15,12 @@
* 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.parser
package com.hippo.ehviewer.client.parser;
import org.jsoup.Jsoup
import com.hippo.util.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
public class EventPaneParser {
public static String parse(String body) {
String event = null;
try {
Document d = Jsoup.parse(body);
Element eventpane = d.getElementById("eventpane");
if (eventpane != null) {
event = eventpane.html();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
}
return event;
object EventPaneParser {
fun parse(body: String): String? {
return Jsoup.parse(body).getElementById("eventpane")?.html()
}
}
@@ -23,7 +23,7 @@ import com.hippo.ehviewer.client.exception.EhException;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.AssertUtilsKt;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -46,7 +46,7 @@ public class FavoritesParser {
//noinspection ConstantConditions
Elements fps = ido.getElementsByClass("fp");
// Last one is "fp fps"
AssertUtils.assertEquals(11, fps.size());
AssertUtilsKt.assertEquals(11, fps.size());
for (int i = 0; i < 10; i++) {
Element fp = fps.get(i);
@@ -36,6 +36,7 @@ import com.hippo.util.ExceptionUtils
import com.hippo.util.JsoupUtils
import com.hippo.yorozuya.NumberUtils
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.trimAnd
import com.hippo.yorozuya.unescapeXml
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
@@ -91,7 +92,8 @@ object GalleryDetailParser {
// Error info
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
val galleryDetail = GalleryDetail()
val document = Jsoup.parse(body)
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
val document = Jsoup.parse(body.replace("del>", "s>"))
parseDetail(galleryDetail, document, body)
galleryDetail.tags = parseTagGroups(document)
galleryDetail.comments = parseComments(document)
@@ -336,7 +338,7 @@ object GalleryDetailParser {
// Id
val a = element.previousElementSibling()
val name = a!!.attr("name")
comment.id = StringUtils.trim(name).substring(1).toInt().toLong()
comment.id = name trimAnd { substring(1).toInt().toLong() }
// Editable, vote up and vote down
val c4 = JsoupUtils.getElementByClass(element, "c4")
if (null != c4) {
@@ -376,15 +378,15 @@ object GalleryDetailParser {
}
// time
val c3 = JsoupUtils.getElementByClass(element, "c3")
var temp = c3!!.ownText()
if (temp.endsWith(':')) {
temp = temp.substring("Posted on ".length, temp.length - " by:".length)
val temp = c3!!.ownText()
val time = if (temp.endsWith(':')) {
// user
comment.user = c3.child(0).text()
temp.substring("Posted on ".length, temp.length - " by:".length)
} else {
temp = temp.substring("Posted on ".length)
temp.substring("Posted on ".length)
}
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
// comment
val c6 = JsoupUtils.getElementByClass(element, "c6")
// fix underline support
@@ -407,7 +409,7 @@ object GalleryDetailParser {
val e = c8.children().first()
if (e != null) {
comment.lastEdited =
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
}
}
comment
@@ -0,0 +1,21 @@
package com.hippo.ehviewer.coil
import coil.disk.DiskCache
inline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {
val editor = edit(key) ?: return false
editor.runCatching {
block(this)
}.onFailure {
editor.abort()
throw it
}.onSuccess {
editor.commit()
}
return true
}
inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {
(get(key) ?: return false).use { block(it) }
return true
}
@@ -0,0 +1,75 @@
package com.hippo.ehviewer.coil
import coil.intercept.Interceptor
import coil.request.ImageRequest
import coil.request.ImageResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
object MergeInterceptor : Interceptor {
private val pendingContinuationMap: HashMap<String, MutableList<Continuation<Unit>>> = hashMapOf()
private val pendingContinuationMapLock = Mutex()
private val notifyScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val originRequest = chain.request
val originListener = originRequest.listener
val key = originRequest.data as String
val newRequest = ImageRequest.Builder(originRequest).listener(
{ originListener?.onStart(it) },
{
originListener?.onCancel(it)
notifyScope.launch {
pendingContinuationMapLock.withLock {
// Wake up a pending continuation to continue executing task
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) }
// If no successor, delete this entry from hashmap
successor ?: pendingContinuationMap.remove(key)
}
}
},
{ request, result ->
originListener?.onError(request, result)
// Wake all pending continuations since this request is to be failed
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
},
{ request, result ->
originListener?.onSuccess(request, result)
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
notifyScope.launch {
pendingContinuationMapLock.withLock {
pendingContinuationMap.remove(key)?.forEach { it.resume(Unit) }
}
}
}
).build()
pendingContinuationMapLock.lock()
val existPendingContinuations = pendingContinuationMap[key]
if (existPendingContinuations == null) {
pendingContinuationMap[key] = mutableListOf()
pendingContinuationMapLock.unlock()
} else {
existPendingContinuations.apply {
suspendCancellableCoroutine { continuation ->
add(continuation)
pendingContinuationMapLock.unlock()
continuation.invokeOnCancellation { remove(continuation) }
}
}
}
return withContext(originRequest.interceptorDispatcher) { chain.proceed(newRequest) }
}
}
@@ -0,0 +1,105 @@
package com.hippo.ehviewer.dailycheck
import android.annotation.SuppressLint
import android.content.Context
import android.text.Html
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import eu.kanade.tachiyomi.util.lang.withIOContext
import java.time.Duration
import java.time.LocalDateTime
private val signedIn
get() = EhApplication.ehCookieStore.hasSignedIn()
private const val CHANNEL_ID = "DailyCheckNotification"
class DailyCheckWork(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = withIOContext {
runCatching {
if (signedIn) {
EhEngine.getNews(true)?.let { showEventPane(it) }
}
}.onFailure {
it.printStackTrace()
return@withIOContext Result.retry()
}
Result.success()
}
@SuppressLint("MissingPermission")
private fun showEventPane(html: String) {
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
return
}
val notificationManager = NotificationManagerCompat.from(context)
val chan = NotificationChannelCompat
.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(CHANNEL_ID)
.build()
notificationManager.createNotificationChannel(chan)
val msg = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_monochrome)
.setContentText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setStyle(NotificationCompat.BigTextStyle())
.build()
runCatching {
notificationManager.notify(1, msg)
}.onFailure {
it.printStackTrace()
}
}
}
val schedHour
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, -1).takeUnless { it == -1 }
val schedMinute
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, -1).takeUnless { it == -1 }
private val whenToWork
get() = LocalDateTime.now()
.run { withHour(schedHour ?: hour) }
.run { withMinute(schedMinute ?: minute) }
private val initialDelay
get() = Duration.between(LocalDateTime.now(), whenToWork)
.run { if (isNegative) plusDays(1) else this }
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
.setConstraints(constraints)
.setInitialDelay(initialDelay)
.build()
}
private const val workName = "DailyCheckWork"
fun updateDailyCheckWork(context: Context) {
if (Settings.getRequestNews()) {
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
workName,
ExistingPeriodicWorkPolicy.UPDATE,
getDailyCheckWorkRequest()
)
} else {
WorkManager.getInstance(context).cancelUniqueWork(workName)
}
}
@@ -37,10 +37,9 @@ import com.hippo.yorozuya.collect.LongList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.util.LinkedList
import kotlin.contracts.contract
object DownloadManager : OnSpiderListener {
private val downloadManager = this
// All download info list
private val mAllInfoList: LinkedList<DownloadInfo>
@@ -68,7 +67,7 @@ object DownloadManager : OnSpiderListener {
init {
// Get all labels
val labels = EhDB.getAllDownloadLabelList()
val labels = EhDB.allDownloadLabelList.toMutableList()
mLabelList = labels
// Create list for each label
@@ -82,7 +81,7 @@ object DownloadManager : OnSpiderListener {
mDefaultInfoList = LinkedList()
// Get all info
val allInfoList = EhDB.getAllDownloadInfo()
val allInfoList = EhDB.allDownloadInfo
mAllInfoList = LinkedList(allInfoList)
// Create all info map
@@ -94,14 +93,21 @@ object DownloadManager : OnSpiderListener {
allInfoMap.put(info.gid, info)
// Add to each label list
var list = getInfoListForLabel(info.label)
if (list == null) {
// Can't find the label in label list
list = LinkedList()
map[info.label] = list
if (!containLabel(info.label)) {
val label = info.label
val list = getInfoListForLabel(label) ?: LinkedList<DownloadInfo>().also {
/* Contracts says if [getInfoListForLabel] return null, the param [label] is not null
* [label] is local immutable
* Why [label] is not smart casted to [String!] here ???
*
* TODO: Report it to kotlin compiler team
* https://youtrack.jetbrains.com/issue/KT-56869
*/
label!!
map[info.label] = it
if (!containLabel(label)) {
// Add label to DB and list
labels.add(EhDB.addDownloadLabel(info.label))
labels.add(EhDB.addDownloadLabel(label))
}
}
list.add(info)
@@ -112,6 +118,9 @@ object DownloadManager : OnSpiderListener {
}
private fun getInfoListForLabel(label: String?): LinkedList<DownloadInfo>? {
contract {
returns(null) implies (label != null)
}
return if (label == null) {
mDefaultInfoList
} else {
@@ -344,7 +353,7 @@ object DownloadManager : OnSpiderListener {
mMap[info.label] = list
if (!containLabel(info.label)) {
// Add label to DB and list
mLabelList.add(EhDB.addDownloadLabel(info.label))
mLabelList.add(EhDB.addDownloadLabel(info.label!!))
}
}
list.add(info)
@@ -838,7 +847,7 @@ object DownloadManager : OnSpiderListener {
override fun onPageFailure(
index: Int,
error: String,
error: String?,
finished: Int,
downloaded: Int,
total: Int
@@ -860,11 +869,11 @@ object DownloadManager : OnSpiderListener {
SimpleHandler.getInstance().post(task)
}
override fun onGetImageSuccess(index: Int, image: Image) {
override fun onGetImageSuccess(index: Int, image: Image?) {
// Ignore
}
override fun onGetImageFailure(index: Int, error: String) {
override fun onGetImageFailure(index: Int, error: String?) {
// Ignore
}
@@ -1080,7 +1089,7 @@ object DownloadManager : OnSpiderListener {
mCurrentSpider = null
// Release spider
if (spider != null) {
spider.removeOnSpiderListener(downloadManager)
spider.removeOnSpiderListener(DownloadManager)
SpiderQueen.releaseSpiderQueen(spider, SpiderQueen.MODE_DOWNLOAD)
}
// Check null
@@ -30,7 +30,6 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
@@ -66,7 +65,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.downloadManager
mDownloadManager = DownloadManager
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
@@ -1,352 +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.gallery;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Process;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.Native;
import com.hippo.UriArchiveAccessor;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
import kotlin.Pair;
public class ArchivePageLoader extends PageLoader2 {
private static final AtomicInteger sIdGenerator = new AtomicInteger();
public static Handler showPasswd;
public static String passwd;
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, Image.CloseableSource> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private Thread archiveThread;
private volatile int size = PageLoader.STATE_WAIT;
private String error;
public ArchivePageLoader(Context context, Uri uri) {
UriArchiveAccessor archiveAccessor1;
try {
archiveAccessor1 = new UriArchiveAccessor(context, uri);
} catch (Exception e) {
archiveAccessor1 = null;
e.printStackTrace();
}
archiveAccessor = archiveAccessor1;
}
@Override
public void start() {
int id = sIdGenerator.incrementAndGet();
archiveThread = new PriorityThread(
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
archiveThread.start();
for (Thread i : decodeThread) {
i.start();
}
}
@Override
public void stop() {
super.stop();
if (archiveThread != null) {
archiveThread.interrupt();
archiveThread = null;
}
for (Thread i : decodeThread) {
i.interrupt();
}
try {
archiveAccessor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int size() {
return size;
}
@Override
protected void onRequest(int index) {
boolean inDecodeTask;
synchronized (streams) {
inDecodeTask = streams.containsKey(index);
}
synchronized (requests) {
boolean inArchiveTask = requests.contains(index);
if (!inArchiveTask && !inDecodeTask) {
requests.add(index);
requests.notify();
}
}
notifyPageWait(index);
}
@Override
protected void onForceRequest(int index) {
onRequest(index);
}
@Override
protected void onCancelRequest(int index) {
synchronized (requests) {
requests.remove(Integer.valueOf(index));
}
}
@Override
public String getError() {
return error;
}
@NonNull
@Override
public String getImageFilename(int index) {
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
}
@Override
public boolean save(int index, @NonNull UniFile file) {
int fd;
FileOutputStream stream;
try {
stream = (FileOutputStream) file.openOutputStream();
fd = Native.getFd(stream.getFD());
} catch (IOException e) {
e.printStackTrace();
return false;
}
archiveAccessor.extractToFd(index, fd);
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
save(index, dst);
return dst;
}
@Override
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask())
};
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
}
private void notifyError() {
showPasswd.sendEmptyMessage(1);
}
private void notifyDismiss() {
showPasswd.sendEmptyMessage(2);
}
@Override
public void run() {
try {
size = archiveAccessor.open();
} catch (Exception e) {
e.printStackTrace();
size = 0;
}
if (size <= 0) {
size = PageLoader.STATE_ERROR;
error = GetText.getString(R.string.error_reading_failed);
notifyDataChanged();
return;
}
// Update size and notify changed
notifyDataChanged();
if (archiveAccessor.needPassword()) {
boolean need_request = true;
Set<String> set = Settings.getArchivePasswds();
if (set != null) {
for (String passwd : set) {
if (archiveAccessor.providePassword(passwd)) {
need_request = false;
break;
}
}
}
while (!Thread.currentThread().isInterrupted() && need_request) {
try {
waitPasswd();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
if (!archiveAccessor.providePassword(passwd))
notifyError();
else {
Settings.putPasswdToArchivePasswds(passwd);
notifyDismiss();
break;
}
}
}
for (Thread i : archiveThreads) {
i.start();
}
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
for (Thread i : archiveThreads) {
i.interrupt();
}
}
}
}
}
private class ArchiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
if (requests.isEmpty()) {
try {
requests.wait();
} catch (InterruptedException e) {
// Interrupted
break;
}
continue;
}
index = requests.pop();
}
// Check index valid
if (index < 0 || index >= size) {
notifyPageFailed(index, null);
continue;
}
synchronized (streams) {
if (streams.get(index) != null) {
continue;
}
}
Image.CloseableSource src = archiveAccessor.getImageSource(index);
synchronized (streams) {
streams.put(index, src);
streams.notify();
}
}
}
}
private class DecodeTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Image.CloseableSource src;
synchronized (streams) {
if (streams.isEmpty()) {
try {
streams.wait();
} catch (InterruptedException e) {
break;
}
continue;
}
Iterator<Map.Entry<Integer, Image.CloseableSource>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Image.CloseableSource> entry = iterator.next();
iterator.remove();
index = entry.getKey();
src = entry.getValue();
}
Image image = null;
if (src != null) {
image = Image.decode(src);
}
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, null);
}
}
}
}
}
@@ -0,0 +1,168 @@
/*
* Copyright 2023 Tarsin Norbin
*
* 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.gallery
import android.content.Context
import android.net.Uri
import com.hippo.Native.getFd
import com.hippo.UriArchiveAccessor
import com.hippo.ehviewer.Settings
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.FileUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import java.io.FileOutputStream
import java.io.IOException
class ArchivePageLoader(context: Context, uri: Uri, passwdFlow: Flow<String>) : PageLoader2(),
CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private val archiveAccessor by lazy { UriArchiveAccessor(context, uri) }
private val hostJob = launch(start = CoroutineStart.LAZY) {
size = archiveAccessor.open()
if (size == 0) {
return@launch
}
archiveAccessor.run {
if (needPassword()) {
Settings.getArchivePasswds()?.forEach {
if (providePassword(it)) return@launch
}
passwdFlow.collect {
if (providePassword(it)) {
Settings.putPasswdToArchivePasswds(it)
currentCoroutineContext().cancel()
}
}
}
}
}
override var size = 0
private set
override fun start() {
hostJob.start()
}
override fun stop() {
cancel()
archiveAccessor.close()
super.stop()
}
private val mJobMap = hashMapOf<Int, Job>()
private val mWorkerMutex by lazy { (0 until size).map { Mutex() } }
private val mSemaphore = Semaphore(4)
override fun onRequest(index: Int) {
synchronized(mJobMap) {
val current = mJobMap[index]
if (current?.isActive != true) {
mJobMap[index] = launch {
mWorkerMutex[index].withLock {
mSemaphore.withPermit {
doRealWork(index)
}
}
}
}
}
}
private suspend fun doRealWork(index: Int) {
val src = archiveAccessor.getImageSource(index) ?: return
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
src.close()
throw it
}
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image.recycle()
throw it
}
notifyPageSucceed(index, image)
}
override fun onForceRequest(index: Int) {
onRequest(index)
}
override suspend fun awaitReady(): Boolean {
hostJob.join()
return size != 0
}
override val isReady: Boolean
get() = size != 0
override fun onCancelRequest(index: Int) {
mJobMap[index]?.cancel()
}
override fun getImageFilename(index: Int): String {
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))
}
override fun getImageFilenameWithExtension(index: Int): String {
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index))
}
override fun save(index: Int, file: UniFile): Boolean {
val fd: Int
val stream: FileOutputStream
try {
stream = file.openOutputStream() as FileOutputStream
fd = getFd(stream.fd)
} catch (e: IOException) {
e.printStackTrace()
return false
}
archiveAccessor.extractToFd(index, fd)
try {
stream.close()
} catch (e: IOException) {
e.printStackTrace()
return false
}
return true
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index))
val dst = dir.subFile(if (null != extension) "$filename.$extension" else filename)
save(index, dst!!)
return dst
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
}
@@ -1,217 +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.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.spider.SpiderQueen;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.SimpleHandler;
import java.util.List;
import java.util.Locale;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
import kotlin.Pair;
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
private final GalleryInfo mGalleryInfo;
@Nullable
private SpiderQueen mSpiderQueen;
public EhPageLoader(GalleryInfo galleryInfo) {
mGalleryInfo = galleryInfo;
}
@Override
public void start() {
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ);
mSpiderQueen.addOnSpiderListener(this);
if (mSpiderQueen.size() > 0)
notifyDataChanged();
}
@Override
public void stop() {
super.stop();
if (mSpiderQueen != null) {
mSpiderQueen.removeOnSpiderListener(this);
// Activity recreate may called, so wait 3000s
SimpleHandler.getInstance().postDelayed(new ReleaseTask(mSpiderQueen), 3000);
mSpiderQueen = null;
}
}
@Override
public int getStartPage() {
if (mSpiderQueen != null) {
return mSpiderQueen.getStartPage();
} else {
return super.getStartPage();
}
}
@NonNull
@Override
public String getImageFilename(int index) {
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@NonNull
@Override
public String getImageFilenameWithExtension(int index) {
if (null != mSpiderQueen) {
String extension = mSpiderQueen.getExtension(index);
if (extension != null) {
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1, extension);
}
}
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
}
@Override
public boolean save(int index, @NonNull UniFile file) {
if (null != mSpiderQueen) {
return mSpiderQueen.save(index, file);
} else {
return false;
}
}
@Nullable
@Override
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
if (null != mSpiderQueen) {
return mSpiderQueen.save(index, dir, filename);
} else {
return null;
}
}
@Override
public void putStartPage(int page) {
if (mSpiderQueen != null) {
mSpiderQueen.putStartPage(page);
}
}
@Override
public int size() {
if (mSpiderQueen != null) {
return mSpiderQueen.size();
} else {
return PageLoader.STATE_ERROR;
}
}
@Override
protected void onRequest(int index) {
if (mSpiderQueen != null) {
mSpiderQueen.request(index);
}
}
@Override
protected void onForceRequest(int index) {
if (mSpiderQueen != null) {
mSpiderQueen.forceRequest(index);
}
}
@Override
protected void onCancelRequest(int index) {
if (mSpiderQueen != null) {
mSpiderQueen.cancelRequest(index);
}
}
@Override
public String getError() {
if (mSpiderQueen != null) {
return mSpiderQueen.getError();
} else {
return "Error"; // TODO
}
}
@Override
public void onGetPages(int pages) {
notifyDataChanged();
}
@Override
public void onGet509(int index) {
// TODO
}
@Override
public void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
if (contentLength > 0) {
notifyPagePercent(index, (float) receivedSize / contentLength);
}
}
@Override
public void onPageSuccess(int index, int finished, int downloaded, int total) {}
@Override
public void onPageFailure(int index, String error, int finished, int downloaded, int total) {
notifyPageFailed(index, error);
}
@Override
public void onFinish(int finished, int downloaded, int total) {
}
@Override
public void onGetImageSuccess(int index, Image image) {
notifyPageSucceed(index, image);
}
@Override
public void onGetImageFailure(int index, String error) {
notifyPageFailed(index, error);
}
@Override
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
if (mSpiderQueen != null) {
mSpiderQueen.preloadPages(pages, pair);
}
}
private static class ReleaseTask implements Runnable {
private SpiderQueen mSpiderQueen;
public ReleaseTask(SpiderQueen spiderQueen) {
mSpiderQueen = spiderQueen;
}
@Override
public void run() {
if (null != mSpiderQueen) {
SpiderQueen.releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ);
mSpiderQueen = null;
}
}
}
}
@@ -0,0 +1,155 @@
/*
* 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.gallery
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.yorozuya.SimpleHandler
import java.util.Locale
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
private lateinit var mSpiderQueen: SpiderQueen
override fun start() {
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
mSpiderQueen.addOnSpiderListener(this)
}
override fun stop() {
super.stop()
mSpiderQueen.removeOnSpiderListener(this)
// Activity recreate may called, so wait 3000s
SimpleHandler.getInstance().postDelayed(ReleaseTask(mSpiderQueen), 3000)
}
override val startPage
get() = mSpiderQueen.startPage
override fun getImageFilename(index: Int): String {
return String.format(
Locale.US,
"%d-%s-%08d",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1
)
}
override fun getImageFilenameWithExtension(index: Int): String {
val extension = mSpiderQueen.getExtension(index)
if (extension != null) {
return String.format(
Locale.US,
"%d-%s-%08d.%s",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1,
extension
)
}
return String.format(
Locale.US,
"%d-%s-%08d",
mGalleryInfo.gid,
mGalleryInfo.token,
index + 1
)
}
override fun save(index: Int, file: UniFile): Boolean {
return mSpiderQueen.save(index, file)
}
override fun save(index: Int, dir: UniFile, filename: String): UniFile? {
return mSpiderQueen.save(index, dir, filename)
}
override fun putStartPage(page: Int) {
mSpiderQueen.putStartPage(page)
}
override val size: Int
get() = mSpiderQueen.size
override fun onRequest(index: Int) {
mSpiderQueen.request(index)
}
override fun onForceRequest(index: Int) {
mSpiderQueen.forceRequest(index)
}
override suspend fun awaitReady(): Boolean {
return mSpiderQueen.awaitReady()
}
override val isReady: Boolean
get() = mSpiderQueen.isReady
override fun onCancelRequest(index: Int) {
mSpiderQueen.cancelRequest(index)
}
override fun onGetPages(pages: Int) {}
override fun onGet509(index: Int) {}
override fun onPageDownload(
index: Int,
contentLength: Long,
receivedSize: Long,
bytesRead: Int
) {
if (contentLength > 0) {
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
}
}
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
override fun onPageFailure(
index: Int,
error: String?,
finished: Int,
downloaded: Int,
total: Int
) {
notifyPageFailed(index, error)
}
override fun onFinish(finished: Int, downloaded: Int, total: Int) {}
override fun onGetImageSuccess(index: Int, image: Image?) {
notifyPageSucceed(index, image!!)
}
override fun onGetImageFailure(index: Int, error: String?) {
notifyPageFailed(index, error)
}
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
mSpiderQueen.preloadPages(pages, pair)
}
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
override fun run() {
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
mSpiderQueen = null
}
}
}
@@ -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.ehviewer.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.unifile.UniFile;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
public abstract class PageLoader2 extends PageLoader {
// With dot
public static final String[] SUPPORT_IMAGE_EXTENSIONS = {
".jpg", // Joint Photographic Experts Group
".png", // Portable Network Graphics
".gif", // Graphics Interchange Format
};
public int getStartPage() {
return 0;
}
public void putStartPage(int page) {
}
/**
* @return without extension
*/
@NonNull
public abstract String getImageFilename(int index);
/**
* @return with extension
*/
@NonNull
public abstract String getImageFilenameWithExtension(int index);
public abstract boolean save(int index, @NonNull UniFile file);
/**
* @param filename without extension
*/
@Nullable
public abstract UniFile save(int index, @NonNull UniFile dir, @NonNull String filename);
}
@@ -0,0 +1,48 @@
/*
* 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.gallery
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
abstract class PageLoader2 : PageLoader() {
open val startPage: Int
get() = 0
open fun putStartPage(page: Int) {}
/**
* @return without extension
*/
abstract fun getImageFilename(index: Int): String
/**
* @return with extension
*/
abstract fun getImageFilenameWithExtension(index: Int): String
abstract fun save(index: Int, file: UniFile): Boolean
/**
* @param filename without extension
*/
abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?
}
val SUPPORT_IMAGE_EXTENSIONS = arrayOf(
".jpg", // Joint Photographic Experts Group
".png", // Portable Network Graphics
".gif"
)
@@ -1,118 +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.preference;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.NumberUtils;
public class CleanRedundancyPreference extends TaskPreference {
public CleanRedundancyPreference(Context context) {
super(context);
}
public CleanRedundancyPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CleanRedundancyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@NonNull
@Override
protected Task onCreateTask() {
return new ClearTask(getContext());
}
private static class ClearTask extends Task {
private final DownloadManager mManager;
public ClearTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager();
}
// True for cleared
private boolean clearFile(UniFile file) {
String name = file.getName();
if (name == null) {
return false;
}
int index = name.indexOf('-');
if (index >= 0) {
name = name.substring(0, index);
}
long gid = NumberUtils.parseLongSafely(name, -1L);
if (-1L == gid) {
return false;
}
if (mManager.containDownloadInfo(gid)) {
return false;
}
file.delete();
return true;
}
@Override
protected Object doInBackground(Void... params) {
UniFile dir = Settings.getDownloadLocation();
if (null == dir) {
return 0;
}
UniFile[] files = dir.listFiles();
if (null == files) {
return 0;
}
int count = 0;
for (UniFile f : files) {
if (clearFile(f)) {
++count;
}
}
return count;
}
@Override
protected void onPostExecute(Object o) {
int count;
if (o instanceof Integer) {
count = (Integer) o;
} else {
count = 0;
}
showTip(0 == count ?
mApplication.getString(R.string.settings_download_clean_redundancy_no_redundancy) :
mApplication.getString(R.string.settings_download_clean_redundancy_done, count), Snackbar.LENGTH_SHORT);
super.onPostExecute(o);
}
}
}
@@ -0,0 +1,74 @@
/*
* 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.preference
import android.content.Context
import android.util.AttributeSet
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
private val NO_REDUNDANCY =
GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)
private val CLEAR_REDUNDANCY_DONE =
{ cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }
private val FINAL_CLEAR_REDUNDANCY_MSG =
{ cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }
class CleanRedundancyPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : TaskPreference(context, attrs) {
private fun clearFile(file: UniFile): Boolean {
var name = file.name ?: return false
val index = name.indexOf('-')
if (index >= 0) {
name = name.substring(0, index)
}
val gid = name.toLongOrNull() ?: return false
if (downloadManager.containDownloadInfo(gid)) {
return false
}
file.delete()
return true
}
private fun doRealWork(): Int {
return Settings.getDownloadLocation()?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0
}
override fun launchJob() {
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
launchUI {
dialog.dismiss()
}
}
else singletonJob = launch {
val cnt = doRealWork()
withUIContext {
showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
dialog.dismiss()
}
}
}
}
private var singletonJob: Job? = null
@@ -15,145 +15,111 @@
*/
package com.hippo.ehviewer.preference
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.readCompatFromUniFile
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import kotlinx.coroutines.runBlocking
import java.io.IOException
import java.util.Collections
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.tarsin.coroutines.runSuspendCatching
class RestoreDownloadPreference constructor(
context: Context, attrs: AttributeSet?
private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)
private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)
private val RESTORE_COUNT_MSG =
{ cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }
class RestoreDownloadPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : TaskPreference(context, attrs) {
override fun onCreateTask(): Task {
return RestoreTask(context)
}
private class RestoreTask(context: Context) : Task(context) {
private val mManager: DownloadManager = downloadManager
private var restoreDirCount = 0
private fun getRestoreItem(file: UniFile?): RestoreItem? {
if (null == file || !file.isDirectory) {
private val mManager: DownloadManager = downloadManager
private var restoreDirCount = 0
private fun getRestoreItem(file: UniFile?): RestoreItem? {
if (null == file || !file.isDirectory) return null
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
return runCatching {
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
var gid = spiderInfo.gid
var dirname = file.name
if (mManager.containDownloadInfo(gid)) {
// Restore download dir to avoid redownload
val dbdirname = EhDB.getDownloadDirname(gid)
if (null == dbdirname || dirname != dbdirname) {
EhDB.putDownloadDirname(gid, dirname)
restoreDirCount++
}
return null
}
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
return try {
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
val gid = spiderInfo.gid
val dirname = file.name
if (mManager.containDownloadInfo(gid)) {
// Restore download dir to avoid redownload
val dbdirname = EhDB.getDownloadDirname(gid)
if (null == dbdirname || dirname != dbdirname) {
EhDB.putDownloadDirname(gid, dirname)
restoreDirCount++
}
return null
}
val token = spiderInfo.token
val restoreItem = RestoreItem()
restoreItem.gid = gid
restoreItem.token = token
restoreItem.dirname = dirname
restoreItem
} catch (e: IOException) {
null
RestoreItem().apply {
gid = spiderInfo.gid
token = spiderInfo.token
dirname = file.name
}
}
}.getOrNull()
}
override fun doInBackground(vararg params: Void): Any? {
val dir = Settings.getDownloadLocation() ?: return null
val restoreItemList: MutableList<RestoreItem> = ArrayList()
val files = dir.listFiles() ?: return null
for (file in files) {
val restoreItem = getRestoreItem(file)
if (null != restoreItem) {
restoreItemList.add(restoreItem)
}
}
return if (0 == restoreItemList.size) {
Collections.EMPTY_LIST
} else try {
runBlocking {
fillGalleryListByApi(
ArrayList(restoreItemList),
EhUrl.referer
)
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
e.printStackTrace()
null
}
}
override fun onPostExecute(o: Any) {
if (o !is List<*>) {
mActivity.showTip(R.string.settings_download_restore_failed, BaseScene.LENGTH_SHORT)
} else {
val list = o as List<RestoreItem>
if (list.isEmpty()) {
mActivity.showTip(
if (restoreDirCount == 0)
mActivity.getString(R.string.settings_download_restore_not_found)
else
mActivity.getString(
R.string.settings_download_restore_successfully,
restoreDirCount
),
BaseScene.LENGTH_SHORT
)
} else {
var count = 0
var i = 0
val n = list.size
while (i < n) {
val item = list[i]
// Avoid failed gallery info
if (null != item.title) {
// Put to download
mManager.addDownload(item, null)
// Put download dir to DB
EhDB.putDownloadDirname(item.gid, item.dirname)
count++
}
i++
}
showTip(
mActivity.getString(
R.string.settings_download_restore_successfully,
count + restoreDirCount
),
BaseScene.LENGTH_SHORT
)
val preference: Preference? = preference
if (null != preference) {
val context = preference.context
if (context is Activity) {
context.setResult(Activity.RESULT_OK)
}
}
}
}
super.onPostExecute(o)
}
private suspend fun doRealWork(): List<GalleryInfo>? {
val dir = Settings.getDownloadLocation() ?: return null
val files = dir.listFiles() ?: return null
val restoreItemList = files.mapNotNull { getRestoreItem(it) }
return runSuspendCatching {
fillGalleryListByApi(restoreItemList, EhUrl.referer)
}.getOrNull()
}
private class RestoreItem : BaseGalleryInfo() {
var dirname: String? = null
}
}
override fun launchJob() {
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
launchUI {
dialog.dismiss()
}
}
else singletonJob = launch {
val result = doRealWork()
withUIContext {
if (result == null) {
showTip(RESTORE_FAILED)
} else {
if (result.isEmpty()) {
showTip(RESTORE_COUNT_MSG(restoreDirCount))
} else {
var count = 0
var i = 0
val n = result.size
while (i < n) {
val item = result[i]
// Avoid failed gallery info
if (null != item.title) {
// Put to download
mManager.addDownload(item, null)
// Put download dir to DB
EhDB.putDownloadDirname(item.gid, (item as RestoreItem).dirname)
count++
}
i++
}
showTip(RESTORE_COUNT_MSG(count + restoreDirCount))
}
}
dialog.dismiss()
}
}
}
}
private var singletonJob: Job? = null
@@ -1,208 +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.preference;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.customview.view.AbsSavedState;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.preference.DialogPreference;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.yorozuya.IntIdGenerator;
public abstract class TaskPreference extends DialogPreference {
private Task mTask;
private int mTaskId = IntIdGenerator.INVALID_ID;
public TaskPreference(Context context) {
super(context);
}
public TaskPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TaskPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle(null);
builder.setView(R.layout.preference_dialog_task);
builder.setCancelable(false);
}
@Override
protected void onDialogCreated(AlertDialog dialog) {
if (null == mTask) {
mTask = onCreateTask();
mTask.setPreference(this);
mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask);
mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance());
}
}
protected void onTaskEnd() {
// Dismiss dialog
Dialog dialog = getDialog();
try {
dialog.dismiss();
} catch (Exception e) {
e.printStackTrace();
}
// Clear async
mTask = null;
mTaskId = IntIdGenerator.INVALID_ID;
}
@NonNull
protected abstract Task onCreateTask();
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState myState = new SavedState(superState);
myState.asyncTaskId = mTaskId;
return myState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
SavedState myState = (SavedState) state;
mTaskId = myState.asyncTaskId;
if (IntIdGenerator.INVALID_ID != mTaskId) {
Object o = ((EhApplication) getContext().getApplicationContext()).getGlobalStuff(mTaskId);
if (o instanceof Task) {
mTask = (Task) o;
mTask.setPreference(this);
}
}
if (null == mTask) {
mTaskId = IntIdGenerator.INVALID_ID;
}
// TODO if not task, show not reopen dialog
super.onRestoreInstanceState(myState.getSuperState());
}
private static class SavedState extends AbsSavedState {
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
int asyncTaskId;
public SavedState(Parcel source) {
super(source, SavedState.class.getClassLoader());
asyncTaskId = source.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(asyncTaskId);
}
}
public abstract static class Task extends AsyncTask<Void, Void, Object> {
protected final EhApplication mApplication;
@SuppressLint("StaticFieldLeak")
protected final SettingsActivity mActivity;
@Nullable
private TaskPreference mPreference;
public Task(@NonNull Context context) {
mActivity = (SettingsActivity) context;
mApplication = (EhApplication) context.getApplicationContext();
}
public EhApplication getApplication() {
return mApplication;
}
@Nullable
public TaskPreference getPreference() {
return mPreference;
}
public void setPreference(@Nullable TaskPreference preference) {
mPreference = preference;
}
@CallSuper
@Override
protected void onPostExecute(Object o) {
mApplication.removeGlobalStuff(this);
if (null != mPreference) {
mPreference.onTaskEnd();
}
}
protected void showTip(@StringRes int id, int length) {
try {
mActivity.showTip(id, length);
} catch (Exception e) {
Toast.makeText(mApplication, id, length).show();
}
}
protected void showTip(CharSequence message, int length) {
try {
mActivity.showTip(message, length);
} catch (Exception e) {
Toast.makeText(mApplication, message, length).show();
}
}
}
}
@@ -0,0 +1,53 @@
/*
* 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.preference
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.SettingsActivity
import com.hippo.preference.DialogPreference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
abstract class TaskPreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : DialogPreference(context, attrs), CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
protected lateinit var dialog: AlertDialog
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
builder.setTitle(null)
builder.setView(R.layout.preference_dialog_task)
builder.setCancelable(false)
}
abstract fun launchJob()
override fun onDialogCreated(dialog: AlertDialog) {
this.dialog = dialog
launchJob()
}
protected fun showTip(msg: String) {
(context as SettingsActivity).showTip(
msg,
Snackbar.LENGTH_SHORT
)
}
}
@@ -16,9 +16,9 @@
package com.hippo.ehviewer.preference
import android.content.Context
import android.content.pm.PackageManager
import android.util.AttributeSet
import androidx.preference.Preference
import com.hippo.ehviewer.BuildConfig
import com.hippo.ehviewer.R
class VersionPreference @JvmOverloads constructor(
@@ -26,13 +26,7 @@ class VersionPreference @JvmOverloads constructor(
) : Preference(context, attrs) {
init {
setTitle(R.string.settings_about_version)
@Suppress("DEPRECATION") val version: String = try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
context.getString(R.string.error_unknown)
}
summary = version
summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" +
context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
}
}
@@ -16,10 +16,13 @@
package com.hippo.ehviewer.spider
import android.graphics.ImageDecoder
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
import coil.decode.DecodeUtils
import coil.decode.FrameDelayRewritingSource
import coil.decode.isGif
import coil.disk.DiskCache
import com.hippo.copyTo
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.EhDB
@@ -28,7 +31,9 @@ import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.gallery.PageLoader2
import com.hippo.ehviewer.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
import com.hippo.image.Image.CloseableSource
import com.hippo.unifile.RawFile
import com.hippo.unifile.UniFile
@@ -36,6 +41,7 @@ import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.MathUtils
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import okio.buffer
import java.io.File
@@ -100,30 +106,27 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
* @param extension with dot
*/
private fun fixExtension(extension: String): String {
return extension.takeIf { PageLoader2.SUPPORT_IMAGE_EXTENSIONS.contains(it) }
?: PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]
return extension.takeIf { SUPPORT_IMAGE_EXTENSIONS.contains(it) }
?: SUPPORT_IMAGE_EXTENSIONS[0]
}
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
val dir = downloadDir ?: return false
val key = EhCacheKeyFactory.getImageKey(mGid, index)
runCatching {
(sCache[key] ?: return false).use { snapshot ->
val extension = fixExtension("." + snapshot.metadata.toFile().readText())
return runCatching {
sCache.read(key) {
val extension = fixExtension("." + metadata.toFile().readText())
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
(file.openOutputStream() as FileOutputStream).use { outputStream ->
outputStream.channel.use { outChannel ->
snapshot.data.toFile().inputStream().use { inputStream ->
inputStream.channel.use {
outChannel.transferFrom(it, 0, it.size())
}
}
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
it copyTo outputStream.fd
}
}
}
return true
}.getOrElse {
it.printStackTrace()
false
}
return false
}
operator fun contains(index: Int): Boolean {
@@ -194,7 +197,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
return receivedSize
}
findDownloadFileForIndex(index, extension)?.runCatching {
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
openOutputStream().use { outputStream ->
(outputStream as FileOutputStream).channel.use { return doSave(it) == length }
}
@@ -206,19 +209,17 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
// Read Mode, allow save to cache
if (mMode == SpiderQueen.MODE_READ) {
val key = EhCacheKeyFactory.getImageKey(mGid, index)
val editor = sCache.edit(key) ?: return false
var received: Long = 0
runCatching {
editor.metadata.toFile().writeText(extension)
editor.data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
runSuspendCatching {
sCache.edit(key) {
metadata.toFile().writeText(extension)
data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
}
}
}.onFailure {
editor.abort()
it.printStackTrace()
return false
}.onSuccess {
editor.commit()
return received == length
}
}
@@ -333,7 +334,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
).build()
}
private val COMPAT_IMAGE_EXTENSIONS = PageLoader2.SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
fun getGalleryDownloadDir(gid: Long): UniFile? {
val dir = Settings.getDownloadLocation()
@@ -1,6 +1,14 @@
package com.hippo.ehviewer.spider
import coil.disk.DiskCache
import com.hippo.compat.readLegacySpiderInfo
import com.hippo.ehviewer.EhApplication
import com.hippo.unifile.UniFile
import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import java.io.File
@Serializable
class SpiderInfo @JvmOverloads constructor(
@@ -18,3 +26,50 @@ class SpiderInfo @JvmOverloads constructor(
var previewPerPage: Int = -1
)
fun SpiderInfo.write(file: UniFile) {
file.openOutputStream().use {
it.write(Cbor.encodeToByteArray(this))
}
}
fun SpiderInfo.saveToCache() {
val entry = spiderInfoCache.edit(gid.toString()) ?: return
runCatching {
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
}.onFailure {
it.printStackTrace()
entry.abort()
}.onSuccess {
entry.commit()
}
}
private val spiderInfoCache by lazy {
DiskCache.Builder()
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
fun readFromCache(gid: Long): SpiderInfo? {
val snapshot = spiderInfoCache[gid.toString()] ?: return null
return runCatching {
snapshot.use {
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
return runCatching {
file.openInputStream().use {
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
}.getOrNull()
}
const val TOKEN_FAILED = "failed"
@@ -1,57 +0,0 @@
package com.hippo.ehviewer.spider
import coil.disk.DiskCache
import com.hippo.compat.readLegacySpiderInfo
import com.hippo.ehviewer.EhApplication
import com.hippo.unifile.UniFile
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import java.io.File
fun SpiderInfo.write(file: UniFile) {
file.openOutputStream().use {
it.write(Cbor.encodeToByteArray(this))
}
}
fun SpiderInfo.saveToCache() {
val entry = spiderInfoCache.edit(gid.toString()) ?: return
runCatching {
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
}.onFailure {
it.printStackTrace()
entry.abort()
}.onSuccess {
entry.commit()
}
}
private val spiderInfoCache by lazy {
DiskCache.Builder()
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
.maxSizeBytes(20 * 1024 * 1024).build()
}
fun readFromCache(gid: Long): SpiderInfo? {
val snapshot = spiderInfoCache[gid.toString()] ?: return null
return runCatching {
snapshot.use {
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
return runCatching {
file.openInputStream().use {
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
}.getOrNull()
}
const val TOKEN_FAILED = "failed"
@@ -1,748 +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.spider;
import android.os.Process;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.collection.LongSparseArray;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhRequestBuilder;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.PreviewSet;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.ehviewer.client.parser.GalleryDetailParser;
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser;
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.util.ExceptionUtils;
import com.hippo.yorozuya.OSUtils;
import com.hippo.yorozuya.thread.PriorityThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
import kotlin.Pair;
import kotlinx.coroutines.CoroutineScopeKt;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public final class SpiderQueen implements Runnable {
public static final int MODE_READ = 0;
public static final int MODE_DOWNLOAD = 1;
public static final int STATE_NONE = 0;
public static final int STATE_DOWNLOADING = 1;
public static final int STATE_FINISHED = 2;
public static final int STATE_FAILED = 3;
public static final String SPIDER_INFO_FILENAME = ".ehviewer";
private static final String TAG = SpiderQueen.class.getSimpleName();
private static final AtomicInteger sIdGenerator = new AtomicInteger();
private static final boolean DEBUG_LOG = true;
private static final boolean DEBUG_PTOKEN = true;
private static final LongSparseArray<SpiderQueen> sQueenMap = new LongSparseArray<>();
public static String PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error);
public static String ERROR_509 = GetText.getString(R.string.error_509);
public static String NETWORK_ERROR = GetText.getString(R.string.error_socket);
public static String DECODE_ERROR = GetText.getString(R.string.error_decoding_failed);
@NonNull
public final SpiderDen mSpiderDen;
public final AtomicReference<SpiderInfo> mSpiderInfo = new AtomicReference<>();
// Store page download percent
public final ConcurrentHashMap<Integer, Float> mPagePercentMap = new ConcurrentHashMap<>();
@NonNull
private final OkHttpClient mHttpClient;
@NonNull
private final GalleryInfo mGalleryInfo;
private final Object mQueenLock = new Object();
private final Object mPageStateLock = new Object();
private final AtomicInteger mDownloadedPages = new AtomicInteger(0);
private final AtomicInteger mFinishedPages = new AtomicInteger(0);
// Store page error
private final ConcurrentHashMap<Integer, String> mPageErrorMap = new ConcurrentHashMap<>();
private final List<OnSpiderListener> mSpiderListeners = new ArrayList<>();
public volatile int[] mPageStateArray;
private int mReadReference = 0;
private int mDownloadReference = 0;
// It mQueenThread is null, failed or stopped
@Nullable
private volatile Thread mQueenThread;
private SpiderQueenWorker mWorkerScope;
private boolean mStoped = false;
private SpiderQueen(@NonNull GalleryInfo galleryInfo) {
mHttpClient = EhApplication.getOkHttpClient();
mGalleryInfo = galleryInfo;
mSpiderDen = new SpiderDen(mGalleryInfo);
mWorkerScope = new SpiderQueenWorker(this);
}
@UiThread
public static SpiderQueen obtainSpiderQueen(@NonNull GalleryInfo galleryInfo, @Mode int mode) {
OSUtils.checkMainLoop();
SpiderQueen queen = sQueenMap.get(galleryInfo.getGid());
if (queen == null) {
queen = new SpiderQueen(galleryInfo);
sQueenMap.put(galleryInfo.getGid(), queen);
// Set mode
queen.setMode(mode);
queen.start();
} else {
// Set mode
queen.setMode(mode);
}
return queen;
}
@UiThread
public static void releaseSpiderQueen(@NonNull SpiderQueen queen, @Mode int mode) {
OSUtils.checkMainLoop();
// Clear mode
queen.clearMode(mode);
if (queen.mReadReference == 0 && queen.mDownloadReference == 0) {
// Stop and remove if there is no reference
queen.stop();
sQueenMap.remove(queen.mGalleryInfo.getGid());
}
}
public static boolean contain(int[] array, int value) {
for (int v : array) {
if (v == value) {
return true;
}
}
return false;
}
// Mark as suspend fun when kotlinize, need IO
public static int getStartPage(long gid) {
var queen = sQueenMap.get(gid);
SpiderInfo spiderInfo = null;
// Fast Path: read existing queen
if (queen != null) {
spiderInfo = queen.mSpiderInfo.get();
}
// Slow path, read diskcache
if (spiderInfo == null) {
spiderInfo = SpiderInfoUtilsKt.readFromCache(gid);
}
if (spiderInfo == null) return 0;
return spiderInfo.getStartPage();
}
public void addOnSpiderListener(OnSpiderListener listener) {
synchronized (mSpiderListeners) {
mSpiderListeners.add(listener);
}
}
public void removeOnSpiderListener(OnSpiderListener listener) {
synchronized (mSpiderListeners) {
mSpiderListeners.remove(listener);
}
}
private void notifyGetPages(int pages) {
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onGetPages(pages);
}
}
}
public void notifyGet509(int index) {
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onGet509(index);
}
}
}
public void notifyPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onPageDownload(index, contentLength, receivedSize, bytesRead);
}
}
}
private void notifyPageSuccess(int index) {
int size = -1;
int[] temp = mPageStateArray;
if (temp != null) {
size = temp.length;
}
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onPageSuccess(index, mFinishedPages.get(), mDownloadedPages.get(), size);
}
}
}
private void notifyPageFailure(int index, String error) {
int size = -1;
int[] temp = mPageStateArray;
if (temp != null) {
size = temp.length;
}
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onPageFailure(index, error, mFinishedPages.get(), mDownloadedPages.get(), size);
}
}
}
private void notifyAllPageDownloaded() {
int size = -1;
int[] temp = mPageStateArray;
if (temp != null) {
size = temp.length;
}
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onFinish(mFinishedPages.get(), mDownloadedPages.get(), size);
}
}
}
public void notifyGetImageSuccess(int index, Image image) {
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onGetImageSuccess(index, image);
}
}
}
public void notifyGetImageFailure(int index, String error) {
if (error == null) {
error = GetText.getString(R.string.error_unknown);
}
synchronized (mSpiderListeners) {
for (OnSpiderListener listener : mSpiderListeners) {
listener.onGetImageFailure(index, error);
}
}
}
private boolean mInDownloadMode = false;
private void updateMode() {
int mode;
if (mDownloadReference > 0) {
mode = MODE_DOWNLOAD;
} else {
mode = MODE_READ;
}
mSpiderDen.setMode(mode);
// Update download page
boolean intoDownloadMode = mode == MODE_DOWNLOAD;
if (intoDownloadMode && !mInDownloadMode && mPageStateArray != null) {
// Clear download state
synchronized (mPageStateLock) {
int[] temp = mPageStateArray;
for (int i = 0, n = temp.length; i < n; i++) {
int oldState = temp[i];
if (STATE_DOWNLOADING != oldState) {
temp[i] = STATE_NONE;
}
}
mDownloadedPages.lazySet(0);
mFinishedPages.lazySet(0);
mPageErrorMap.clear();
mPagePercentMap.clear();
}
}
mInDownloadMode = intoDownloadMode;
}
private void setMode(@Mode int mode) {
switch (mode) {
case MODE_READ -> mReadReference++;
case MODE_DOWNLOAD -> mDownloadReference++;
}
if (mDownloadReference > 1) {
throw new IllegalStateException("mDownloadReference can't more than 0");
}
updateMode();
}
private void clearMode(@Mode int mode) {
switch (mode) {
case MODE_READ -> mReadReference--;
case MODE_DOWNLOAD -> mDownloadReference--;
}
if (mReadReference < 0 || mDownloadReference < 0) {
throw new IllegalStateException("Mode reference < 0");
}
updateMode();
}
private void start() {
Thread queenThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(),
Process.THREAD_PRIORITY_BACKGROUND);
mQueenThread = queenThread;
queenThread.start();
}
private void stop() {
mStoped = true;
synchronized (mQueenLock) {
mQueenLock.notifyAll();
}
CoroutineScopeKt.cancel(mWorkerScope, null);
mWorkerScope = null;
}
public int size() {
if (mQueenThread == null) {
return PageLoader.STATE_ERROR;
} else if (mPageStateArray == null) {
return PageLoader.STATE_WAIT;
} else {
return mPageStateArray.length;
}
}
public String getError() {
if (mQueenThread == null) {
return "Error";
} else {
return null;
}
}
public void forceRequest(int index) {
request(index, true);
}
public void request(int index) {
request(index, false);
}
private int getPageState(int index) {
synchronized (mPageStateLock) {
if (mPageStateArray != null && index >= 0 && index < mPageStateArray.length) {
return mPageStateArray[index];
} else {
return STATE_NONE;
}
}
}
public void cancelRequest(int index) {
if (mQueenThread == null) {
return;
}
mWorkerScope.cancelDecode(index);
}
public void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
if (mQueenThread == null) {
return;
}
mWorkerScope.updateRAList(pages, pair);
}
private void request(int index, boolean force) {
if (mQueenThread == null) {
return;
}
// Get page state
int state = getPageState(index);
// Fix state for force
if ((force && (state == STATE_FINISHED || state == STATE_FAILED)) || (state == STATE_FAILED)) {
// Update state to none at once
updatePageState(index, STATE_NONE);
}
mWorkerScope.launch(index, force);
}
public boolean save(int index, @NonNull UniFile file) {
int state = getPageState(index);
if (STATE_FINISHED != state) {
return false;
}
return mSpiderDen.saveToUniFile(index, file);
}
@Nullable
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
int state = getPageState(index);
if (STATE_FINISHED != state) {
return null;
}
var ext = mSpiderDen.getExtension(index);
UniFile dst = dir.subFile(null != ext ? filename + "." + ext : filename);
if (null == dst) {
return null;
}
if (!mSpiderDen.saveToUniFile(index, dst)) return null;
return dst;
}
@Nullable
public String getExtension(int index) {
int state = getPageState(index);
if (STATE_FINISHED != state) {
return null;
}
return mSpiderDen.getExtension(index);
}
public int getStartPage() {
SpiderInfo spiderInfo = mSpiderInfo.get();
if (spiderInfo == null) spiderInfo = readSpiderInfoFromLocal();
if (spiderInfo == null) return 0;
return spiderInfo.getStartPage();
}
public void putStartPage(int page) {
final SpiderInfo spiderInfo = mSpiderInfo.get();
if (spiderInfo != null) {
spiderInfo.setStartPage(page);
}
}
private synchronized SpiderInfo readSpiderInfoFromLocal() {
SpiderInfo spiderInfo = mSpiderInfo.get();
if (spiderInfo != null) {
return spiderInfo;
}
// Read from download dir
UniFile downloadDir = mSpiderDen.getDownloadDir();
if (downloadDir != null) {
UniFile file = downloadDir.findFile(SPIDER_INFO_FILENAME);
if (file != null) {
spiderInfo = SpiderInfoUtilsKt.readCompatFromUniFile(file);
if (spiderInfo != null && spiderInfo.getGid() == mGalleryInfo.getGid() &&
spiderInfo.getToken().equals(mGalleryInfo.getToken())) {
return spiderInfo;
}
}
}
// Read from cache
spiderInfo = SpiderInfoUtilsKt.readFromCache(mGalleryInfo.getGid());
if (spiderInfo != null && spiderInfo.getGid() == mGalleryInfo.getGid() && spiderInfo.getToken().equals(mGalleryInfo.getToken())) {
return spiderInfo;
}
return null;
}
private void readPreviews(String body, int index, SpiderInfo spiderInfo) throws ParseException {
spiderInfo.setPreviewPages(GalleryDetailParser.parsePreviewPages(body));
PreviewSet previewSet = GalleryDetailParser.parsePreviewSet(body);
if (previewSet.size() > 0) {
if (index == 0) {
spiderInfo.setPreviewPerPage(previewSet.size());
} else {
spiderInfo.setPreviewPerPage(previewSet.getPosition(0) / index);
}
}
for (int i = 0, n = previewSet.size(); i < n; i++) {
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i));
if (result != null) {
spiderInfo.getPTokenMap().put(result.page, result.pToken);
}
}
}
private SpiderInfo readSpiderInfoFromInternet() {
Request request = new EhRequestBuilder(EhUrl.getGalleryDetailUrl(
mGalleryInfo.getGid(), mGalleryInfo.getToken(), 0, false), EhUrl.getReferer()).build();
try (Response response = mHttpClient.newCall(request).execute()) {
String body = response.body().string();
var pages = GalleryDetailParser.parsePages(body);
SpiderInfo spiderInfo = new SpiderInfo(mGalleryInfo.getGid(), pages);
spiderInfo.setToken(mGalleryInfo.getToken());
readPreviews(body, 0, spiderInfo);
return spiderInfo;
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return null;
}
}
public String getPTokenFromMultiPageViewer(int index) {
SpiderInfo spiderInfo = mSpiderInfo.get();
if (spiderInfo == null) {
return null;
}
String url = EhUrl.getGalleryMultiPageViewerUrl(
mGalleryInfo.getGid(), mGalleryInfo.getToken());
if (DEBUG_PTOKEN) {
Log.d(TAG, "getPTokenFromMultiPageViewer index " + index + ", url " + url);
}
String referer = EhUrl.getReferer();
Request request = new EhRequestBuilder(url, referer).build();
try (Response response = mHttpClient.newCall(request).execute()) {
String body = response.body().string();
ArrayList<String> list = GalleryMultiPageViewerPTokenParser.parse(body);
for (int i = 0; i < list.size(); i++) {
spiderInfo.getPTokenMap().put(i, list.get(i));
}
return spiderInfo.getPTokenMap().get(index);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return null;
}
}
public String getPTokenFromInternet(int index) {
SpiderInfo spiderInfo = mSpiderInfo.get();
if (spiderInfo == null) {
return null;
}
// Check previewIndex
int previewIndex;
if (spiderInfo.getPreviewPerPage() >= 0) {
previewIndex = index / spiderInfo.getPreviewPerPage();
} else {
previewIndex = 0;
}
if (spiderInfo.getPreviewPages() > 0) {
previewIndex = Math.min(previewIndex, spiderInfo.getPreviewPages() - 1);
}
String url = EhUrl.getGalleryDetailUrl(
mGalleryInfo.getGid(), mGalleryInfo.getToken(), previewIndex, false);
String referer = EhUrl.getReferer();
if (DEBUG_PTOKEN) {
Log.d(TAG, "index " + index + ", previewIndex " + previewIndex +
", previewPerPage " + spiderInfo.getPreviewPerPage() + ", url " + url);
}
Request request = new EhRequestBuilder(url, referer).build();
try (Response response = mHttpClient.newCall(request).execute()) {
String body = response.body().string();
readPreviews(body, previewIndex, spiderInfo);
return spiderInfo.getPTokenMap().get(index);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return null;
}
}
private synchronized void writeSpiderInfoToLocal() {
var spiderInfo = mSpiderInfo.get();
if (spiderInfo != null) {
UniFile downloadDir = mSpiderDen.getDownloadDir();
if (downloadDir != null) {
UniFile file = downloadDir.createFile(SPIDER_INFO_FILENAME);
SpiderInfoUtilsKt.write(spiderInfo, file);
}
SpiderInfoUtilsKt.saveToCache(spiderInfo);
}
}
private void runInternal() {
// Read spider info
SpiderInfo spiderInfo = readSpiderInfoFromLocal();
// Check Stopped
if (mStoped) {
return;
}
// Spider info from internet
if (spiderInfo == null) {
spiderInfo = readSpiderInfoFromInternet();
}
// Error! Can't get spiderInfo
if (spiderInfo == null) {
return;
}
mSpiderInfo.set(spiderInfo);
// Check Stopped
if (mStoped) {
return;
}
// Setup page state
synchronized (mPageStateLock) {
mPageStateArray = new int[spiderInfo.getPages()];
}
// Notify get pages
notifyGetPages(spiderInfo.getPages());
if (mInDownloadMode) mWorkerScope.enterDownloadMode();
// Wait finish
synchronized (mQueenLock) {
try {
mQueenLock.wait();
} catch (InterruptedException ignored) {
}
}
writeSpiderInfoToLocal();
}
@Override
public void run() {
if (DEBUG_LOG) {
Log.i(TAG, Thread.currentThread().getName() + ": start");
}
runInternal();
// Set mQueenThread null
mQueenThread = null;
if (DEBUG_LOG) {
Log.i(TAG, Thread.currentThread().getName() + ": end");
}
}
public void updatePageState(int index, @State int state) {
updatePageState(index, state, null);
}
private boolean isStateDone(int state) {
return state == STATE_FINISHED || state == STATE_FAILED;
}
public void updatePageState(int index, @State int state, String error) {
int oldState;
synchronized (mPageStateLock) {
oldState = mPageStateArray[index];
mPageStateArray[index] = state;
if (!isStateDone(oldState) && isStateDone(state)) {
mDownloadedPages.incrementAndGet();
} else if (isStateDone(oldState) && !isStateDone(state)) {
mDownloadedPages.decrementAndGet();
}
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
mFinishedPages.incrementAndGet();
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
mFinishedPages.decrementAndGet();
}
// Clear
if (state == STATE_DOWNLOADING) {
mPageErrorMap.remove(index);
} else if (state == STATE_FINISHED || state == STATE_FAILED) {
mPagePercentMap.remove(index);
}
// Get default error
if (state == STATE_FAILED) {
if (error == null) {
error = GetText.getString(R.string.error_unknown);
}
mPageErrorMap.put(index, error);
}
}
// Notify listeners
if (state == STATE_FAILED) {
notifyPageFailure(index, error);
} else if (state == STATE_FINISHED) {
notifyPageSuccess(index);
}
if (mFinishedPages.get() == size()) notifyAllPageDownloaded();
}
@IntDef({MODE_READ, MODE_DOWNLOAD})
@Retention(RetentionPolicy.SOURCE)
public @interface Mode {
}
@IntDef({STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
}
public interface OnSpiderListener {
void onGetPages(int pages);
void onGet509(int index);
/**
* @param contentLength -1 for unknown
*/
void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead);
void onPageSuccess(int index, int finished, int downloaded, int total);
void onPageFailure(int index, String error, int finished, int downloaded, int total);
/**
* All workers end
*/
void onFinish(int finished, int downloaded, int total);
void onGetImageSuccess(int index, Image image);
void onGetImageFailure(int index, String error);
}
}
@@ -0,0 +1,830 @@
/*
* Copyright 2016 Hippo Seven
* Rewrite with Kotlin coroutines, Tarsin Norbin 2023
*
* 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.spider
import android.util.Log
import androidx.annotation.IntDef
import androidx.collection.LongSparseArray
import androidx.collection.set
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
import com.hippo.ehviewer.GetText
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
import com.hippo.ehviewer.client.EhUrl.referer
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewPages
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewSet
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
import com.hippo.image.Image
import com.hippo.unifile.UniFile
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
import okhttp3.executeAsync
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
@Volatile
lateinit var mPageStateArray: IntArray
lateinit var mSpiderInfo: SpiderInfo
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
val mPagePercentMap = ConcurrentHashMap<Int, Float>()
private val mPageStateLock = Any()
private val mDownloadedPages = AtomicInteger(0)
private val mFinishedPages = AtomicInteger(0)
private val mPageErrorMap = ConcurrentHashMap<Int, String>()
private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()
private var mReadReference = 0
private var mDownloadReference = 0
fun addOnSpiderListener(listener: OnSpiderListener) {
synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }
}
fun removeOnSpiderListener(listener: OnSpiderListener) {
synchronized(mSpiderListeners) { mSpiderListeners.remove(listener) }
}
private fun notifyGetPages(pages: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGetPages(pages) }
}
}
fun notifyGet509(index: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach { it.onGet509(index) }
}
}
fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageDownload(
index,
contentLength,
receivedSize,
bytesRead
)
}
}
}
private fun notifyPageSuccess(index: Int) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageSuccess(
index,
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
private fun notifyPageFailure(index: Int, error: String?) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onPageFailure(
index,
error,
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
private fun notifyAllPageDownloaded() {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onFinish(
mFinishedPages.get(),
mDownloadedPages.get(),
mPageStateArray.size
)
}
}
}
fun notifyGetImageSuccess(index: Int, image: Image) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onGetImageSuccess(index, image)
}
}
}
fun notifyGetImageFailure(index: Int, error: String) {
synchronized(mSpiderListeners) {
mSpiderListeners.forEach {
it.onGetImageFailure(index, error)
}
}
}
private var downloadMode = false
val isReady
get() = this::mSpiderInfo.isInitialized && this::mPageStateArray.isInitialized
@Synchronized
private fun updateMode() {
if (!isReady) return
val mode: Int = if (mDownloadReference > 0) {
MODE_DOWNLOAD
} else {
MODE_READ
}
mSpiderDen.setMode(mode)
// Update download page
val intoDownloadMode = mode == MODE_DOWNLOAD
if (intoDownloadMode && !downloadMode) {
// Clear download state
synchronized(mPageStateLock) {
val temp: IntArray = mPageStateArray
var i = 0
val n = temp.size
while (i < n) {
val oldState = temp[i]
if (STATE_DOWNLOADING != oldState) {
temp[i] = STATE_NONE
}
i++
}
mDownloadedPages.lazySet(0)
mFinishedPages.lazySet(0)
mPageErrorMap.clear()
mPagePercentMap.clear()
}
mWorkerScope.enterDownloadMode()
}
downloadMode = intoDownloadMode
}
private fun setMode(@Mode mode: Int) {
when (mode) {
MODE_READ -> mReadReference++
MODE_DOWNLOAD -> mDownloadReference++
}
check(mDownloadReference <= 1) { "mDownloadReference can't more than 0" }
}
private fun clearMode(@Mode mode: Int) {
when (mode) {
MODE_READ -> mReadReference--
MODE_DOWNLOAD -> mDownloadReference--
}
check(!(mReadReference < 0 || mDownloadReference < 0)) { "Mode reference < 0" }
}
private val prepareJob = launchIO { doPrepare() }
private suspend fun doPrepare() {
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
mPageStateArray = IntArray(mSpiderInfo.pages)
notifyGetPages(mSpiderInfo.pages)
}
suspend fun awaitReady(): Boolean {
prepareJob.join()
return isReady
}
suspend fun awaitStartPage(): Int {
prepareJob.join()
if (!isReady) return 0
return mSpiderInfo.startPage
}
private fun stop() {
launchNonCancellable { runCatching { writeSpiderInfoToLocal() } }
cancel()
}
val size
get() = mPageStateArray.size
val error: String?
get() = null
fun forceRequest(index: Int) {
request(index, true)
}
fun request(index: Int) {
request(index, false)
}
private fun getPageState(index: Int): Int {
synchronized(mPageStateLock) {
return if (index >= 0 && index < mPageStateArray.size) {
mPageStateArray[index]
} else {
STATE_NONE
}
}
}
fun cancelRequest(index: Int) {
mWorkerScope.cancelDecode(index)
}
fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
mWorkerScope.updateRAList(pages, pair)
}
private fun request(index: Int, force: Boolean) {
// Get page state
val state = getPageState(index)
// Fix state for force
if (force && (state == STATE_FINISHED || state == STATE_FAILED) || state == STATE_FAILED) {
// Update state to none at once
updatePageState(index, STATE_NONE)
}
mWorkerScope.launch(index, force)
}
fun save(index: Int, file: UniFile): Boolean {
val state = getPageState(index)
return if (STATE_FINISHED != state) {
false
} else mSpiderDen.saveToUniFile(index, file)
}
fun save(index: Int, dir: UniFile, filename: String): UniFile? {
val state = getPageState(index)
if (STATE_FINISHED != state) {
return null
}
val ext = mSpiderDen.getExtension(index)
val dst = dir.subFile(if (null != ext) "$filename.$ext" else filename) ?: return null
return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst
}
fun getExtension(index: Int): String? {
val state = getPageState(index)
return if (STATE_FINISHED != state) {
null
} else mSpiderDen.getExtension(index)
}
val startPage: Int
get() = mSpiderInfo.startPage
fun putStartPage(page: Int) {
mSpiderInfo.startPage = page
}
private fun readSpiderInfoFromLocal(): SpiderInfo? {
return mSpiderDen.downloadDir?.run {
findFile(SPIDER_INFO_FILENAME)?.let { file ->
readCompatFromUniFile(file)?.takeIf {
it.gid == galleryInfo.gid && it.token == galleryInfo.token
}
}
}
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
}
@Throws(ParseException::class)
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
spiderInfo.previewPages = parsePreviewPages(body)
val previewSet = parsePreviewSet(body)
if (previewSet.size() > 0) {
if (index == 0) {
spiderInfo.previewPerPage = previewSet.size()
} else {
spiderInfo.previewPerPage = previewSet.getPosition(0) / index
}
}
var i = 0
val n = previewSet.size()
while (i < n) {
val result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i))
if (result != null) {
spiderInfo.pTokenMap[result.page] = result.pToken
}
i++
}
}
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
val request = EhRequestBuilder(
getGalleryDetailUrl(
galleryInfo.gid, galleryInfo.token, 0, false
), referer
).build()
return runSuspendCatching {
okHttpClient.newCall(request).executeAsync().use { response ->
val body = response.body.string()
val pages = parsePages(body)
val spiderInfo = SpiderInfo(galleryInfo.gid, pages)
spiderInfo.token = galleryInfo.token
readPreviews(body, 0, spiderInfo)
spiderInfo
}
}.onFailure {
it.printStackTrace()
}.getOrNull()
}
fun getPTokenFromMultiPageViewer(index: Int): String? {
val spiderInfo = mSpiderInfo
val url = getGalleryMultiPageViewerUrl(
galleryInfo.gid, galleryInfo.token!!
)
val referer = referer
val request = EhRequestBuilder(url, referer).build()
try {
okHttpClient.newCall(request).execute().use { response ->
val body = response.body.string()
val list = GalleryMultiPageViewerPTokenParser.parse(body)
for (i in list.indices) {
spiderInfo.pTokenMap[i] = list[i]
}
return spiderInfo.pTokenMap[index]
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
return null
}
}
fun getPTokenFromInternet(index: Int): String? {
val spiderInfo = mSpiderInfo
// Check previewIndex
var previewIndex: Int
previewIndex = if (spiderInfo.previewPerPage >= 0) {
index / spiderInfo.previewPerPage
} else {
0
}
if (spiderInfo.previewPages > 0) {
previewIndex = previewIndex.coerceAtMost(spiderInfo.previewPages - 1)
}
val url = getGalleryDetailUrl(
galleryInfo.gid, galleryInfo.token, previewIndex, false
)
val referer = referer
val request = EhRequestBuilder(url, referer).build()
try {
okHttpClient.newCall(request).execute().use { response ->
val body = response.body.string()
readPreviews(body, previewIndex, spiderInfo)
return spiderInfo.pTokenMap[index]
}
} catch (e: Throwable) {
ExceptionUtils.throwIfFatal(e)
return null
}
}
@Synchronized
private fun writeSpiderInfoToLocal() {
if (!isReady) return
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
mSpiderInfo.saveToCache()
}
private fun isStateDone(state: Int): Boolean {
return state == STATE_FINISHED || state == STATE_FAILED
}
@JvmOverloads
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
var oldState: Int
synchronized(mPageStateLock) {
oldState = mPageStateArray[index]
mPageStateArray[index] = state
if (!isStateDone(oldState) && isStateDone(state)) {
mDownloadedPages.incrementAndGet()
} else if (isStateDone(oldState) && !isStateDone(state)) {
mDownloadedPages.decrementAndGet()
}
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
mFinishedPages.incrementAndGet()
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
mFinishedPages.decrementAndGet()
}
// Clear
if (state == STATE_DOWNLOADING) {
mPageErrorMap.remove(index)
} else if (isStateDone(state)) {
mPagePercentMap.remove(index)
}
// Get default error
if (state == STATE_FAILED) {
mPageErrorMap[index] = error ?: GetText.getString(R.string.error_unknown)
}
}
// Notify listeners
if (state == STATE_FAILED) {
notifyPageFailure(index, error)
} else if (state == STATE_FINISHED) {
notifyPageSuccess(index)
}
if (mDownloadedPages.get() == size) notifyAllPageDownloaded()
}
@IntDef(MODE_READ, MODE_DOWNLOAD)
@Retention
annotation class Mode
@IntDef(STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED)
@Retention(AnnotationRetention.SOURCE)
annotation class State
interface OnSpiderListener {
fun onGetPages(pages: Int)
fun onGet509(index: Int)
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int)
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int)
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int)
fun onFinish(finished: Int, downloaded: Int, total: Int)
fun onGetImageSuccess(index: Int, image: Image?)
fun onGetImageFailure(index: Int, error: String?)
}
companion object {
const val MODE_READ = 0
const val MODE_DOWNLOAD = 1
const val STATE_NONE = 0
const val STATE_DOWNLOADING = 1
const val STATE_FINISHED = 2
const val STATE_FAILED = 3
const val SPIDER_INFO_FILENAME = ".ehviewer"
private val sQueenMap = LongSparseArray<SpiderQueen>()
@JvmStatic
fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {
val gid = galleryInfo.gid
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
setMode(mode)
launchIO { if (awaitReady()) updateMode() }
}
}
@JvmStatic
fun releaseSpiderQueen(queen: SpiderQueen, @Mode mode: Int) {
queen.run {
clearMode(mode)
if (mReadReference == 0 && mDownloadReference == 0) {
stop()
sQueenMap.remove(galleryInfo.gid)
} else {
launchIO { if (awaitReady()) updateMode() }
}
}
}
}
private val mWorkerScope = object {
private val mFetcherJobMap = hashMapOf<Int, Job>()
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
private val pTokenLock = Mutex()
private var showKey: String? = null
private val showKeyLock = Mutex()
private val mDownloadDelay = Settings.getDownloadDelay()
private var isDownloadMode = false
fun cancelDecode(index: Int) {
decoder.cancel(index)
}
@Synchronized
fun enterDownloadMode() {
if (isDownloadMode) return
updateRAList((0 until size).toList())
isDownloadMode = true
}
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
if (isDownloadMode) return
synchronized(mFetcherJobMap) {
mFetcherJobMap.forEach { (i, job) ->
if (i < cancelBounds.first || i > cancelBounds.second)
job.cancel()
}
list.forEach {
if (mFetcherJobMap[it]?.isActive != true)
doLaunchDownloadJob(it, false)
}
}
}
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
val state = mPageStateArray[index]
if (!force && state == STATE_FINISHED) return
val currentJob = mFetcherJobMap[index]
if (force) currentJob?.cancel()
if (currentJob?.isActive != true) {
mFetcherJobMap[index] = launch {
mSemaphore.withPermit {
doInJob(index, force)
}
}.apply {
invokeOnCompletion {
if (getPageState(index) == STATE_DOWNLOADING) {
Log.d(WORKER_DEBUG_TAG, "Download image cancelled $index")
mSpiderDen.remove(index)
}
}
}
}
}
fun launch(index: Int, force: Boolean = false) {
check(index in 0 until size)
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
if (force) decoder.cancel(index)
decoder.launch(index)
}
private suspend fun doInJob(index: Int, force: Boolean) {
fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: getPTokenFromInternet(index)
?: getPTokenFromInternet(index)
?: getPTokenFromMultiPageViewer(index)
}
updatePageState(index, STATE_DOWNLOADING)
if (!force && index in mSpiderDen) {
return updatePageState(index, STATE_FINISHED)
}
if (force) {
pTokenLock.withLock {
val pToken = mSpiderInfo.pTokenMap[index]
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
}
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
pToken = getPToken(index) ?: return updatePageState(
index,
STATE_FAILED,
PTOKEN_FAILED_MESSAGE
).also {
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
}
previousPToken = getPToken(index - 1)
}
var skipHathKey: String? = null
val skipHathKeys = mutableListOf<String>()
var originImageUrl: String? = null
var error: String? = null
var forceHtml = false
var leakSkipHathKey = false
repeat(3) {
var imageUrl: String? = null
var localShowKey: String?
showKeyLock.withLock {
localShowKey = showKey
if (localShowKey == null || forceHtml) {
if (leakSkipHathKey) return@repeat
var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
// Add skipHathKey
if (skipHathKey != null) {
pageUrl += if ("?" in pageUrl) {
"&nl=$skipHathKey"
} else {
"?nl=$skipHathKey"
}
}
runSuspendCatching {
EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)
.also {
if (check509(it.imageUrl)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onSuccess { result ->
imageUrl = result.imageUrl
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
originImageUrl = result.originImageUrl
localShowKey = result.showKey
if (skipHathKey != null) {
if (skipHathKey in skipHathKeys) {
// Duplicate skip hath key
leakSkipHathKey = true
} else {
skipHathKeys.add(skipHathKey!!)
}
} else {
leakSkipHathKey = true
}
showKey = result.showKey
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
return@repeat
} else {
error = ExceptionUtils.getReadableString(it)
return@repeat
}
}
}
}
if (imageUrl == null) {
if (localShowKey == null) {
error = "ShowKey error"
return@repeat
}
runSuspendCatching {
EhEngine.getGalleryPageApi(
mSpiderInfo.gid,
index,
pToken,
localShowKey,
previousPToken
).also {
if (check509(it.imageUrl)) {
// Get 509
notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
} else {
error = ExceptionUtils.getReadableString(it)
}
return@repeat
}.onSuccess {
imageUrl = it.imageUrl
skipHathKey = it.skipHathKey
originImageUrl = it.originImageUrl
}
}
val targetImageUrl: String?
val referer: String?
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
targetImageUrl = originImageUrl
referer = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
} else {
targetImageUrl = imageUrl
referer = null
}
if (targetImageUrl == null) {
error = "TargetImageUrl error"
return@repeat
}
Log.d(WORKER_DEBUG_TAG, targetImageUrl)
runSuspendCatching {
Log.d(WORKER_DEBUG_TAG, "Start download image $index")
val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage(
index,
targetImageUrl,
referer
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
mPagePercentMap[index] = receivedSize.toFloat() / contentLength
notifyPageDownload(index, contentLength, receivedSize, bytesRead)
}
if (!success) {
Log.e(WORKER_DEBUG_TAG, "Can't download all of image data")
error = "Incomplete"
forceHtml = true
return@repeat
}
if (mSpiderDen.checkPlainText(index)) {
error = ""
forceHtml = true
return@repeat
}
Log.d(WORKER_DEBUG_TAG, "Download image succeed $index")
updatePageState(index, STATE_FINISHED)
delay(mDownloadDelay.toLong())
return
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
forceHtml = true
}
Log.d(WORKER_DEBUG_TAG, "End download image $index")
}
mSpiderDen.remove(index)
updatePageState(index, STATE_FAILED, error)
}
private val decoder = object {
private val mSemaphore = Semaphore(4)
private val mDecodeJobMap = hashMapOf<Int, Job>()
fun cancel(index: Int) {
synchronized(mDecodeJobMap) {
mDecodeJobMap.remove(index)?.cancel()
}
}
fun launch(index: Int) {
synchronized(mDecodeJobMap) {
val currentJob = mDecodeJobMap[index]
if (currentJob?.isActive != true) {
mDecodeJobMap[index] = launch {
doInJob(index)
}
}
}
}
private suspend fun doInJob(index: Int) {
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
val src = mSpiderDen.getImageSource(index) ?: return
val image = mSemaphore.withPermit { Image.decode(src) }
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image?.recycle()
throw it
}
if (image == null) {
notifyGetImageFailure(index, DECODE_ERROR)
} else {
notifyGetImageSuccess(index, image)
}
}
}
}
}
private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
private val ERROR_509 = GetText.getString(R.string.error_509)
private val NETWORK_ERROR = GetText.getString(R.string.error_socket)
private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
private val URL_509_ARRAY = arrayOf(
"https://ehgt.org/g/509.gif",
"https://ehgt.org/g/509s.gif",
"https://exhentai.org/img/509.gif",
"https://exhentai.org/img/509s.gif"
)
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
private fun check509(url: String) = url in URL_509_ARRAY
@@ -1,329 +0,0 @@
package com.hippo.ehviewer.spider
import android.util.Log
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine.getGalleryPage
import com.hippo.ehviewer.client.EhEngine.getGalleryPageApi
import com.hippo.ehviewer.client.EhUrl.getPageUrl
import com.hippo.ehviewer.client.exception.ParseException
import com.hippo.ehviewer.spider.SpiderQueen.DECODE_ERROR
import com.hippo.ehviewer.spider.SpiderQueen.ERROR_509
import com.hippo.ehviewer.spider.SpiderQueen.NETWORK_ERROR
import com.hippo.ehviewer.spider.SpiderQueen.PTOKEN_FAILED_MESSAGE
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FAILED
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FINISHED
import com.hippo.image.Image.Companion.decode
import com.hippo.util.ExceptionUtils
import com.hippo.yorozuya.StringUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import moe.tarsin.coroutines.runSuspendCatching
import kotlin.coroutines.CoroutineContext
class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
private val spiderDen
get() = queen.mSpiderDen
private val spiderInfo by lazy { queen.mSpiderInfo.get() }
private val mFetcherJobMap = hashMapOf<Int, Job>()
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
private val pTokenLock = Mutex()
private var showKey: String? = null
private val showKeyLock = Mutex()
private val mDownloadDelay = Settings.getDownloadDelay()
private var isDownloadMode = false
private val size
get() = queen.size()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + Job()
fun cancelDecode(index: Int) {
decoder.cancel(index)
}
@Synchronized
fun enterDownloadMode() {
if (isDownloadMode) return
updateRAList((0 until size).toList())
isDownloadMode = true
}
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
if (isDownloadMode) return
synchronized(mFetcherJobMap) {
mFetcherJobMap.forEach { (i, job) ->
if (i < cancelBounds.first || i > cancelBounds.second)
job.cancel()
}
list.forEach {
if (mFetcherJobMap[it]?.isActive != true)
doLaunchDownloadJob(it, false)
}
}
}
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
val state = queen.mPageStateArray[index]
if (!force && state == STATE_FINISHED) return
val currentJob = mFetcherJobMap[index]
if (force) currentJob?.cancel()
if (currentJob?.isActive != true) {
mFetcherJobMap[index] = launch {
mSemaphore.withPermit {
doInJob(index, force)
}
}
}
}
@JvmOverloads
fun launch(index: Int, force: Boolean = false) {
check(index in 0 until size)
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
if (force) decoder.cancel(index)
decoder.launch(index)
}
private suspend fun doInJob(index: Int, force: Boolean) {
fun getPToken(index: Int): String? {
if (index !in 0 until size) return null
return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
?: queen.getPTokenFromInternet(index)
?: queen.getPTokenFromInternet(index)
?: queen.getPTokenFromMultiPageViewer(index)
}
queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING)
if (!force && index in spiderDen) {
return queen.updatePageState(index, STATE_FINISHED)
}
if (force) {
pTokenLock.withLock {
val pToken = spiderInfo.pTokenMap[index]
if (pToken == TOKEN_FAILED) spiderInfo.pTokenMap.remove(index)
}
}
val previousPToken: String?
val pToken: String
pTokenLock.withLock {
pToken = getPToken(index) ?: return queen.updatePageState(
index,
STATE_FAILED,
PTOKEN_FAILED_MESSAGE
).also {
spiderInfo.pTokenMap[index] = TOKEN_FAILED
}
previousPToken = getPToken(index - 1)
}
var skipHathKey: String? = null
val skipHathKeys = mutableListOf<String>()
var originImageUrl: String? = null
var error: String? = null
var forceHtml = false
var leakSkipHathKey = false
repeat(3) {
var imageUrl: String? = null
var localShowKey: String?
showKeyLock.withLock {
localShowKey = showKey
if (localShowKey == null || forceHtml) {
if (leakSkipHathKey) return@repeat
var pageUrl = getPageUrl(spiderInfo.gid, index, pToken)
// Add skipHathKey
if (skipHathKey != null) {
pageUrl += if ("?" in pageUrl) {
"&nl=$skipHathKey"
} else {
"?nl=$skipHathKey"
}
}
runSuspendCatching {
getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onSuccess { result ->
imageUrl = result.imageUrl
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
originImageUrl = result.originImageUrl
localShowKey = result.showKey
if (skipHathKey != null) {
if (skipHathKey in skipHathKeys) {
// Duplicate skip hath key
leakSkipHathKey = true
} else {
skipHathKeys.add(skipHathKey!!)
}
} else {
leakSkipHathKey = true
}
showKey = result.showKey
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
return@repeat
} else {
error = ExceptionUtils.getReadableString(it)
return@repeat
}
}
}
}
if (imageUrl == null) {
if (localShowKey == null) {
error = "ShowKey error"
return@repeat
}
runSuspendCatching {
getGalleryPageApi(
spiderInfo.gid,
index,
pToken,
localShowKey,
previousPToken
).also {
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
// Get 509
queen.notifyGet509(index)
error = ERROR_509
return@repeat
}
}
}.onFailure {
if (it is ParseException && "Key mismatch" == it.message) {
// Show key is wrong, enter a new loop to get the new show key
if (showKey == localShowKey) showKey = null
} else {
error = ExceptionUtils.getReadableString(it)
}
return@repeat
}.onSuccess {
imageUrl = it.imageUrl
skipHathKey = it.skipHathKey
originImageUrl = it.originImageUrl
}
}
val targetImageUrl: String?
val referer: String?
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
targetImageUrl = originImageUrl
referer = getPageUrl(spiderInfo.gid, index, pToken)
} else {
targetImageUrl = imageUrl
referer = null
}
if (targetImageUrl == null) {
error = "TargetImageUrl error"
return@repeat
}
Log.d(DEBUG_TAG, targetImageUrl)
runSuspendCatching {
Log.d(DEBUG_TAG, "Start download image $index")
val success: Boolean = spiderDen.makeHttpCallAndSaveImage(
index,
targetImageUrl,
referer
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
queen.mPagePercentMap[index] = receivedSize.toFloat() / contentLength
queen.notifyPageDownload(index, contentLength, receivedSize, bytesRead)
}
if (!success) {
Log.e(DEBUG_TAG, "Can't download all of image data")
error = "Incomplete"
forceHtml = true
return@repeat
}
if (spiderDen.checkPlainText(index)) {
error = ""
forceHtml = true
return@repeat
}
Log.d(DEBUG_TAG, "Download image succeed $index")
queen.updatePageState(index, STATE_FINISHED)
delay(mDownloadDelay.toLong())
return
}.onFailure {
it.printStackTrace()
error = NETWORK_ERROR
}
Log.d(DEBUG_TAG, "End download image $index")
}
spiderDen.remove(index)
queen.updatePageState(index, STATE_FAILED, error)
}
companion object {
private val URL_509_SUFFIX_ARRAY = arrayOf(
"/509.gif",
"/509s.gif"
)
private const val DEBUG_TAG = "SpiderQueenWorker"
}
private val decoder = Decoder()
inner class Decoder {
private val mSemaphore = Semaphore(4)
private val mDecodeJobMap = hashMapOf<Int, Job>()
fun cancel(index: Int) {
synchronized(mDecodeJobMap) {
mDecodeJobMap.remove(index)?.cancel()
}
}
fun launch(index: Int) {
synchronized(mDecodeJobMap) {
val currentJob = mDecodeJobMap[index]
if (currentJob?.isActive != true) {
mDecodeJobMap[index] = launch {
doInJob(index)
}
}
}
}
private suspend fun doInJob(index: Int) {
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
val src = spiderDen.getImageSource(index) ?: return
val image = mSemaphore.withPermit { decode(src) }
runCatching {
currentCoroutineContext().ensureActive()
}.onFailure {
image?.recycle()
throw it
}
if (image == null) {
queen.notifyGetImageFailure(index, DECODE_ERROR)
} else {
queen.notifyGetImageSuccess(index, image)
}
}
}
}
@@ -22,7 +22,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import com.hippo.app.ListCheckBoxDialogBuilder
import com.hippo.ehviewer.EhApplication.Companion.application
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
@@ -89,8 +89,8 @@ import java.io.OutputStream
class MainActivity : EhActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private lateinit var connectivityManager: ConnectivityManager
private val availableNetworks: MutableList<Network> = mutableListOf()
private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
private val availableNetworks = mutableListOf<Network>()
private fun saveImageToTempFile(uri: Uri): File? {
val src = ImageDecoder.createSource(contentResolver, uri)
@@ -223,7 +223,6 @@ class MainActivity : EhActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
connectivityManager = getSystemService()!!
if (Settings.getDF() && Settings.getBypassVpn()) {
bypassVpn()
}
@@ -233,13 +232,7 @@ class MainActivity : EhActivity() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navController = navHostFragment.navController
if (EhUtils.needSignedIn()) {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setNavGraph()
}.launch(Intent(this, LoginActivity::class.java))
} else {
setNavGraph()
}
if (!EhUtils.needSignedIn()) setNavGraph()
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
binding.navView.setupWithNavController(navController)
@@ -371,7 +364,19 @@ class MainActivity : EhActivity() {
}
}
private val loginLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (availableNetworks.isNotEmpty()) {
connectivityManager.bindProcessToNetwork(availableNetworks.last())
}
setNavGraph()
}
override fun onResume() {
if (EhUtils.needSignedIn()) {
connectivityManager.bindProcessToNetwork(null)
loginLauncher.launch(Intent(this, LoginActivity::class.java))
}
super.onResume()
lifecycleScope.launch {
delay(100)
@@ -16,7 +16,6 @@
package com.hippo.ehviewer.ui.fragment
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -41,10 +40,11 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
import com.hippo.ehviewer.client.parser.FavoritesParser
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.util.ExceptionUtils
import com.hippo.util.IoThreadPoolExecutor
import com.hippo.util.LogCat
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.IOUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -79,10 +79,9 @@ class AdvancedFragment : BasePreferenceFragment() {
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show()
IoThreadPoolExecutor.getInstance().execute {
lifecycleScope.launchIO {
val success = EhDB.exportDB(requireActivity(), uri)
val activity: Activity? = activity
activity?.runOnUiThread {
withUIContext {
if (alertDialog.isShowing) {
alertDialog.dismiss()
}
@@ -204,10 +203,9 @@ class AdvancedFragment : BasePreferenceFragment() {
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show()
IoThreadPoolExecutor.getInstance().execute {
lifecycleScope.launchIO {
val error = EhDB.importDB(requireActivity(), uri)
val activity: Activity? = activity
activity?.runOnUiThread {
withUIContext {
if (alertDialog.isShowing) {
alertDialog.dismiss()
}
@@ -23,12 +23,17 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat.CLOCK_12H
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.dailycheck.schedHour
import com.hippo.ehviewer.dailycheck.schedMinute
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
class EhFragment : BasePreferenceFragment() {
@@ -57,6 +62,7 @@ class EhFragment : BasePreferenceFragment() {
thumbResolution!!.setSummaryProvider {
getString(R.string.settings_eh_thumb_resolution_summary, (it as ListPreference).entry)
}
if (!EhTagDatabase.isTranslatable(requireActivity())) {
preferenceScreen.removePreference(showTagTranslations)
preferenceScreen.removePreference(tagTranslationsSource!!)
@@ -66,6 +72,27 @@ class EhFragment : BasePreferenceFragment() {
val preference = findPreference<Preference>(it)
preferenceScreen.removePreference(preference!!)
}
} else {
findPreference<Preference>(Settings.KEY_REQUEST_NEWS_TIMER)!!.apply {
setOnPreferenceClickListener {
MaterialTimePicker.Builder()
.apply {
schedHour?.let { setHour(it) }
schedMinute?.let { setMinute(it) }
}
.setTimeFormat(CLOCK_12H)
.build()
.apply {
addOnPositiveButtonClickListener {
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, hour)
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, minute)
updateDailyCheckWork(requireContext())
}
}
.show(childFragmentManager, null)
false
}
}
}
}
@@ -105,6 +132,10 @@ class EhFragment : BasePreferenceFragment() {
EhApplication.application.recreateAllActivity()
}
return true
} else if (Settings.KEY_REQUEST_NEWS == key) {
updateDailyCheckWork(requireContext())
} else if (Settings.KEY_REQUEST_NEWS_TIMER == key) {
updateDailyCheckWork(requireContext())
}
return true
}
@@ -415,7 +415,7 @@ class FilterFragment : BaseFragment() {
mEhFilter.deleteFilter(filter)
}
fun trigger(filter: Filter?) {
fun trigger(filter: Filter) {
mEhFilter.triggerFilter(filter)
}
}
@@ -78,6 +78,10 @@ suspend fun postLogin() = coroutineScope {
}
launch {
runCatching {
// For the `star` cookie
EhEngine.getNews(false)
// Sad panda check
Settings.putGallerySite(EhUrl.SITE_EX)
EhEngine.getUConfig()
}.onFailure {
@@ -51,7 +51,7 @@ import com.hippo.easyrecyclerview.FastScroller
import com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener
import com.hippo.easyrecyclerview.HandlerDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -70,7 +70,6 @@ import com.hippo.widget.FabLayout.OnClickFabListener
import com.hippo.widget.FabLayout.OnExpandListener
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.ObjectUtils
import com.hippo.yorozuya.ViewUtils
@@ -152,8 +151,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = context
AssertUtils.assertNotNull(context)
mDownloadManager = downloadManager
mDownloadManager!!.addDownloadInfoListener(this)
if (savedInstanceState == null) {
@@ -249,10 +246,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
container!!.addView(mFabLayout)
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
mViewTransition = ViewTransition(content, mTip)
val context = context
AssertUtils.assertNotNull(content)
val resources = context!!.resources
val drawable = ContextCompat.getDrawable(context, R.drawable.big_download)
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_download)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mAdapter = DownloadAdapter()
@@ -422,8 +416,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
container: ViewGroup?, savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.drawer_list_rv, container, false)
val context = context
AssertUtils.assertNotNull(context)
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
toolbar.setTitle(R.string.download_labels)
toolbar.inflateMenu(R.menu.drawer_download)
@@ -431,7 +423,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
val id = item.itemId
if (id == R.id.action_add) {
val builder =
EditTextDialogBuilder(context!!, null, getString(R.string.download_labels))
EditTextDialogBuilder(requireContext(), null, getString(R.string.download_labels))
builder.setTitle(R.string.new_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
@@ -449,7 +441,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
items[i + 2] = list[i].label
i++
}
BaseDialogBuilder(context!!)
BaseDialogBuilder(requireContext())
.setTitle(R.string.default_download_label)
.setItems(items) { _: DialogInterface?, which: Int ->
if (which == 0) {
@@ -994,7 +986,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
private val mListThumbHeight: Int
init {
AssertUtils.assertNotNull(mInflater)
@SuppressLint("InflateParams") val calculator =
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -63,7 +63,6 @@ import com.hippo.widget.ContentLayout
import com.hippo.widget.FabLayout
import com.hippo.widget.FabLayout.OnClickFabListener
import com.hippo.widget.FabLayout.OnExpandListener
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.ObjectUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.ViewUtils
@@ -132,8 +131,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
private var mModifyAdd = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = context
AssertUtils.assertNotNull(context)
mClient = EhClient
mFavLocalCount = Settings.getFavLocalCount()
mFavCountSum = Settings.getFavCloudCount()
@@ -186,7 +183,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
val view = inflater.inflate(R.layout.scene_favorites, container, false)
val mContentLayout = view.findViewById<ContentLayout>(R.id.content_layout)
val activity = mainActivity!!
AssertUtils.assertNotNull(activity)
setOnApplySearch { query: String? ->
onApplySearch(query)
}
@@ -196,7 +192,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout
mFabLayout!!.addOnExpandListener(FabLayoutListener())
(mFabLayout!!.parent as ViewGroup).removeView(mFabLayout)
AssertUtils.assertNotNull(container)
container!!.addView(mFabLayout)
ViewCompat.setWindowInsetsAnimationCallback(
view, WindowInsetsAnimationHelper(
@@ -204,9 +199,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
mFabLayout
)
)
val context = context
AssertUtils.assertNotNull(context)
val resources = context!!.resources
val paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
mHelper = FavoritesHelper()
mHelper!!.setEmptyString(resources.getString(R.string.gallery_list_empty_hit))
@@ -332,9 +324,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.drawer_list_rv, container, false)
val context = context
val toolbar = ViewUtils.`$$`(view, R.id.toolbar) as Toolbar
AssertUtils.assertNotNull(context)
toolbar.setTitle(R.string.collections)
toolbar.inflateMenu(R.menu.drawer_favorites)
toolbar.setOnMenuItemClickListener { item: MenuItem ->
@@ -345,7 +335,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
items[1] = getString(R.string.local_favorites)
val favCat = Settings.getFavCat()
System.arraycopy(favCat, 0, items, 2, 10)
BaseDialogBuilder(context!!)
BaseDialogBuilder(requireContext())
.setTitle(R.string.default_favorites_collection)
.setItems(items) { _: DialogInterface?, which: Int ->
Settings.putDefaultFavSlot(
@@ -656,8 +646,8 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
private fun onGetFavoritesLocal(keyword: String?, taskId: Int) {
if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {
val list: List<GalleryInfo> = if (TextUtils.isEmpty(keyword)) {
EhDB.getAllLocalFavorites()
val list: List<GalleryInfo> = if (keyword.isNullOrEmpty()) {
EhDB.allLocalFavorites
} else {
EhDB.searchLocalFavorites(keyword)
}
@@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.hippo.drawable.TriangleDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
@@ -56,7 +56,7 @@ import com.hippo.app.BaseDialogBuilder
import com.hippo.app.CheckBoxDialogBuilder
import com.hippo.app.EditTextDialogBuilder
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhDB
@@ -89,6 +89,7 @@ import com.hippo.ehviewer.databinding.SceneGalleryDetailBinding
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.spider.SpiderQueen
import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
import com.hippo.ehviewer.ui.MainActivity
@@ -106,7 +107,6 @@ import com.hippo.widget.AutoWrapLayout
import com.hippo.widget.LoadImageView
import com.hippo.widget.ObservedTextView
import com.hippo.widget.SimpleGridAutoSpanLayout
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import com.hippo.yorozuya.ViewUtils
@@ -264,7 +264,9 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
mGalleryInfo?.let {
viewLifecycleOwner.lifecycleScope.launchIO {
runCatching {
val startPage = SpiderQueen.getStartPage(it.gid)
val queen = SpiderQueen.obtainSpiderQueen(it, MODE_READ)
val startPage = queen.awaitStartPage()
SpiderQueen.releaseSpiderQueen(queen, MODE_READ)
withUIContext {
binding.content.header.read.text = if (startPage == 0) {
getString(R.string.read)
@@ -361,8 +363,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
// Get download state
val gid = gid
mDownloadState = if (gid != -1L) {
val context = context
AssertUtils.assertNotNull(context)
downloadManager.getDownloadState(gid)
} else {
DownloadInfo.STATE_INVALID
@@ -426,8 +426,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
override fun onDestroyView() {
super.onDestroyView()
val context = context
AssertUtils.assertNotNull(context)
downloadManager.removeDownloadInfoListener(this)
(requireActivity() as MainActivity).mShareUrl = null
mViewTransition = null
@@ -436,8 +434,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
}
private fun prepareData(): Boolean {
val context = context
AssertUtils.assertNotNull(context)
if (mGalleryDetail != null) {
return true
}
@@ -451,7 +447,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
if (mGalleryDetail != null) {
return true
}
val application = context!!.applicationContext as EhApplication
val application = requireContext().applicationContext as EhApplication
return if (application.containGlobalStuff(mRequestId)) {
// request exist
true
@@ -574,7 +570,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
_binding ?: return
val resources = resources
binding.content.header.run {
(thumb as LoadImageView).load(gd.thumb!!, true)
(thumb as LoadImageView).load(gd.thumb!!, false)
setTitle(EhUtils.getSuitableTitle(gd))
uploader.text = gd.uploader
if (gd.disowned) {
@@ -729,14 +725,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
for (i in 0 until previewSet.size()) {
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
val image = view.findViewById<LoadImageView>(R.id.image)
previewSet.load(image, gd.gid, i)
image.setTag(R.id.index, i)
withUIContext {
gridLayout.addView(view)
image.setTag(R.id.index, i)
image.setOnClickListener(this@GalleryDetailScene)
val text = view.findViewById<TextView>(R.id.text)
text.text = (previewSet.getPosition(i) + 1).toString()
}
previewSet.load(image, gd.gid, i)
}
}
}
@@ -1197,7 +1193,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
if (null == context || -1L == gid) {
return
}
val downloadState = downloadManager.getDownloadState(gid)
val downloadState = com.hippo.ehviewer.download.DownloadManager.getDownloadState(gid)
if (downloadState == mDownloadState) {
return
}
@@ -62,7 +62,7 @@ import com.hippo.app.EditTextDialogBuilder
import com.hippo.drawable.AddDeleteDrawable
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.FavouriteStatusRouter
@@ -74,7 +74,9 @@ import com.hippo.ehviewer.client.EhRequest
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.ListUrlBuilder
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
import com.hippo.ehviewer.client.exception.EhException
@@ -98,13 +100,14 @@ import com.hippo.widget.FabLayout
import com.hippo.widget.FabLayout.OnClickFabListener
import com.hippo.widget.FabLayout.OnExpandListener
import com.hippo.yorozuya.AnimationUtils
import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.SimpleAnimatorListener
import com.hippo.yorozuya.StringUtils
import com.hippo.yorozuya.ViewUtils
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.delay
import rikka.core.res.resolveColor
import java.io.File
import java.time.LocalDateTime
@@ -216,6 +219,13 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
}
val goto = args?.getString(KEY_GOTO)
goto?.let {
lifecycleScope.launchUI {
delay(100)
mHelper?.goTo(it, true)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -288,25 +298,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
})
}
private fun wrapTagKeyword(keyword: String?): String {
var keyword = keyword
keyword = keyword!!.trim { it <= ' ' }
val index1 = keyword.indexOf(':')
if (index1 == -1 || index1 >= keyword.length - 1) {
// Can't find :, or : is the last char
return keyword
}
if (keyword[index1 + 1] == '"') {
// The char after : is ", the word must be quoted
return keyword
}
val index2 = keyword.indexOf(' ')
return if (index2 <= index1) {
// Can't find space, or space is before :
keyword
} else keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\""
}
// Update search bar title, drawer checked item
private fun onUpdateUrlBuilder() {
_binding ?: return
@@ -332,8 +323,8 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
// Update search edit text
if (!mIsTopList) {
if (mode == ListUrlBuilder.MODE_TAG) {
keyword = wrapTagKeyword(keyword)
if (mode == MODE_TAG) {
keyword = wrapTagKeyword(keyword!!)
}
setSearchBarText(keyword)
}
@@ -546,14 +537,15 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val qsDrawerAdapter = QsDrawerAdapter(inflater)
qsDrawerAdapter.setHasStableIds(true)
drawerBinding.recyclerViewDrawer.adapter = qsDrawerAdapter
val itemTouchHelper = ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
if (!mIsTopList) {
val itemTouchHelper =
ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
drawerBinding.tip.visibility = View.VISIBLE
drawerBinding.recyclerViewDrawer.visibility = View.GONE
}
lifecycleScope.launchIO {
mQuickSearchList = EhDB.getAllQuickSearch()
mQuickSearchList = EhDB.allQuickSearch.toMutableList()
if (mQuickSearchList.isNotEmpty()) {
withUIContext {
drawerBinding.tip.visibility = View.GONE
@@ -995,7 +987,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val oldMode = mUrlBuilder.mode
// If it's MODE_SUBSCRIPTION, keep it
val newMode =
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else ListUrlBuilder.MODE_NORMAL
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
mUrlBuilder.reset()
mUrlBuilder.mode = newMode
mUrlBuilder.keyword = query
@@ -1155,8 +1147,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
val goto = if (i != -1) q.name!!.substring(i + 1) else null
val args = ListUrlBuilder().apply {
set(q)
setIndex(goto)
}.toStartArgs()
}.toStartArgs().apply { putString(KEY_GOTO, goto) }
navigate(R.id.galleryListScene, args, true)
setState(STATE_NORMAL)
closeDrawer(GravityCompat.END)
@@ -1431,6 +1422,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
companion object {
const val KEY_ACTION = "action"
const val KEY_GOTO = "goto"
const val ACTION_HOMEPAGE = "action_homepage"
const val ACTION_SUBSCRIPTION = "action_subscription"
const val ACTION_WHATS_HOT = "action_whats_hot"
@@ -1444,38 +1436,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
private const val STATE_SEARCH = 2
private const val STATE_SEARCH_SHOW_LIST = 3
private const val ANIMATE_TIME = 300L
private fun getSuitableTitleForUrlBuilder(
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
): String? {
val keyword = urlBuilder.keyword
val category = urlBuilder.category
return if (ListUrlBuilder.MODE_NORMAL == urlBuilder.mode && EhUtils.NONE == category &&
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
) {
resources.getString(if (appName) R.string.app_name else R.string.homepage)
} else if (MODE_SUBSCRIPTION == urlBuilder.mode && EhUtils.NONE == category &&
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
) {
resources.getString(R.string.subscription)
} else if (MODE_WHATS_HOT == urlBuilder.mode) {
resources.getString(R.string.whats_hot)
} else if (MODE_TOPLIST == urlBuilder.mode) {
when (urlBuilder.keyword) {
"11" -> resources.getString(R.string.toplist_alltime)
"12" -> resources.getString(R.string.toplist_pastyear)
"13" -> resources.getString(R.string.toplist_pastmonth)
"15" -> resources.getString(R.string.toplist_yesterday)
else -> null
}
} else if (!TextUtils.isEmpty(keyword)) {
keyword
} else if (MathUtils.hammingWeight(category) == 1) {
EhUtils.getCategory(category)
} else {
null
}
}
fun ListUrlBuilder.toStartArgs(): Bundle {
val args = Bundle()
args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)
@@ -1483,4 +1443,40 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
return args
}
}
}
private fun getSuitableTitleForUrlBuilder(
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
): String? {
val keyword = urlBuilder.keyword
val category = urlBuilder.category
val mode = urlBuilder.mode
return if (mode == MODE_WHATS_HOT) {
resources.getString(R.string.whats_hot)
} else if (!keyword.isNullOrEmpty()) {
when (mode) {
MODE_TOPLIST -> {
when (keyword) {
"11" -> resources.getString(R.string.toplist_alltime)
"12" -> resources.getString(R.string.toplist_pastyear)
"13" -> resources.getString(R.string.toplist_pastmonth)
"15" -> resources.getString(R.string.toplist_yesterday)
else -> null
}
}
MODE_TAG -> wrapTagKeyword(keyword)
else -> keyword
}
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
when (mode) {
MODE_NORMAL -> resources.getString(if (appName) R.string.app_name else R.string.homepage)
MODE_SUBSCRIPTION -> resources.getString(R.string.subscription)
else -> null
}
} else if (category.countOneBits() == 1) {
EhUtils.getCategory(category)
} else {
null
}
}
@@ -43,6 +43,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.databinding.SceneHistoryBinding
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
@@ -73,7 +74,7 @@ class HistoryScene : BaseToolbarScene() {
}
})
}
private val mDownloadManager = EhApplication.downloadManager
private val mDownloadManager = DownloadManager
private val mDownloadInfoListener: DownloadInfoListener by lazy {
object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
@@ -131,7 +132,7 @@ class HistoryScene : BaseToolbarScene() {
val historyData = Pager(
PagingConfig(20)
) {
EhDB.getHistoryLazyList()
EhDB.historyLazyList
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
binding.recyclerView.adapter = mAdapter
val layoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
@@ -296,7 +297,7 @@ class HistoryScene : BaseToolbarScene() {
}
3 -> {
val labelRawList = EhApplication.downloadManager.labelList
val labelRawList = DownloadManager.labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
labelList.add(getString(R.string.default_download_label_name))
var i = 0
@@ -362,7 +363,7 @@ class HistoryScene : BaseToolbarScene() {
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val downloadManager = EhApplication.downloadManager
val downloadManager = DownloadManager
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
downloadManager.changeLabel(listOf(downloadInfo), label)
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhRequest
import com.hippo.util.ExceptionUtils
import com.hippo.view.ViewTransition
import com.hippo.yorozuya.AssertUtils
import com.hippo.yorozuya.ViewUtils
/**
@@ -130,9 +129,7 @@ class ProgressScene : BaseScene(), View.OnClickListener {
val view = inflater.inflate(R.layout.scene_progress, container, false)
val progress = ViewUtils.`$$`(view, R.id.progress)
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
val context = context
AssertUtils.assertNotNull(context)
val drawable = ContextCompat.getDrawable(context!!, R.drawable.big_sad_pandroid)
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mTip!!.setOnClickListener(this)
@@ -127,6 +127,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
fun setSearchBarText(text: String?) {
binding.toolbar.text = text
binding.searchview.setText(text)
}
fun setEditTextHint(hint: String?) {
@@ -168,18 +169,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
mAllowEmptySearch = allowEmptySearch
}
private fun wrapTagKeyword(keyword: String): String {
return if (keyword.endsWith(':')) {
keyword
} else if (keyword.contains(' ')) {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length)
"$prefix\"$tag$\" "
} else {
"$keyword$ "
}
}
interface SuggestionProvider {
fun providerSuggestions(text: String): List<Suggestion>?
@@ -268,7 +257,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
edittext.let {
val keywords = it.text.toString().substringBeforeLast(' ', "")
val keyword = wrapTagKeyword(mKeyword)
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword" else keyword
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword " else "$keyword "
it.setText(newKeywords)
it.setSelection(newKeywords.length)
}
@@ -370,3 +359,15 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
}
}
}
fun wrapTagKeyword(keyword: String): String {
return if (keyword.endsWith(':')) {
keyword
} else if (keyword.contains(' ')) {
val tag = keyword.substringAfter(':')
val prefix = keyword.dropLast(tag.length)
"$prefix\"$tag$\""
} else {
"$keyword$"
}
}
@@ -61,7 +61,7 @@ fun HistoryScreen(hostFragment: HistoryComposeScreenFragmentBridge) {
Pager(
PagingConfig(20)
) {
EhDB.getHistoryLazyList()
EhDB.historyLazyList
}.flow
}.collectAsLazyPagingItems()
@@ -27,15 +27,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import eu.kanade.tachiyomi.util.system.pxToDp
import java.util.Locale
private val downloadManager = EhApplication.downloadManager
private val downloadManager = DownloadManager
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -25,18 +25,18 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.download.DownloadManager
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
private val downloadManager = EhApplication.downloadManager
private val downloadManager = DownloadManager
@Composable
private fun DialogSelectorItem(
@@ -73,9 +73,4 @@ public class FixedThumb extends LoadImageView {
setScaleType(ScaleType.FIT_CENTER);
}
@Override
public void onPreSetImageResource(int resId, boolean isTarget) {
setScaleType(ScaleType.FIT_CENTER);
}
}
+1 -1
View File
@@ -56,8 +56,8 @@ class Image private constructor(private val src: CloseableSource) {
(mObtainedDrawable as? AnimatedImageDrawable)?.stop()
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
mObtainedDrawable?.callback = null
mObtainedDrawable = null
(mObtainedDrawable as? AnimatedImageDrawable)?.let { src.close() }
mObtainedDrawable = null
}
companion object {
@@ -1,92 +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.util;
import androidx.annotation.NonNull;
import com.hippo.yorozuya.thread.PriorityThreadFactory;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class IoThreadPoolExecutor extends ThreadPoolExecutor {
private final static ThreadPoolExecutor INSTANCE =
IoThreadPoolExecutor.newInstance(3, 32, 1L, TimeUnit.SECONDS,
new PriorityThreadFactory("IO", android.os.Process.THREAD_PRIORITY_BACKGROUND));
private IoThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
public static ThreadPoolExecutor getInstance() {
return INSTANCE;
}
private static ThreadPoolExecutor newInstance(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
ThreadFactory threadFactory
) {
ThreadQueue queue = new ThreadQueue();
PutRunnableBackHandler handler = new PutRunnableBackHandler();
IoThreadPoolExecutor executor = new IoThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit, queue, threadFactory, handler);
queue.setThreadPoolExecutor(executor);
return executor;
}
private static class ThreadQueue extends LinkedBlockingQueue<Runnable> {
private ThreadPoolExecutor executor;
void setThreadPoolExecutor(ThreadPoolExecutor executor) {
this.executor = executor;
}
@Override
public boolean offer(@NonNull Runnable o) {
int allWorkingThreads = executor.getActiveCount() + super.size();
return allWorkingThreads < executor.getPoolSize() && super.offer(o);
}
}
public static class PutRunnableBackHandler implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
throw new RejectedExecutionException(e);
}
}
}
}
@@ -1,78 +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.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Log;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class PackageUtils {
private static final String TAG = PackageUtils.class.getSimpleName();
public static String getSignature(Context context, String packageName) {
try {
@SuppressLint("PackageManagerGetSignatures")
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
Signature[] ss = pi.signatures;
if (ss != null && ss.length >= 1) {
return computeSHA1(ss[0].toByteArray());
} else {
Log.e(TAG, "Can't find signature in package " + packageName);
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
Log.e(TAG, "Can't find package " + packageName, e);
}
return null;
}
/**
* @return looks like A1:43:6B:34... or null
*/
public static String computeSHA1(final byte[] certRaw) {
StringBuilder sb = new StringBuilder(59);
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA1");
byte[] sha1 = md.digest(certRaw);
int length = sha1.length;
for (int i = 0; i < length; i++) {
if (i != 0) {
sb.append(':');
}
byte b = sha1[i];
String appendStr = Integer.toString(b & 0xff, 16);
if (appendStr.length() == 1) {
sb.append(0);
}
sb.append(appendStr);
}
return sb.toString().toUpperCase();
} catch (NoSuchAlgorithmException ex) {
Log.e(TAG, "Can't final Algorithm SHA1", ex);
return null;
}
}
}
@@ -17,99 +17,49 @@
package com.hippo.widget
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.TransitionDrawable
import android.util.AttributeSet
import android.view.View
import androidx.annotation.IntDef
import androidx.core.content.ContextCompat
import coil.imageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import androidx.appcompat.content.res.AppCompatResources
import coil.load
import coil.size.Size
import com.hippo.drawable.PreciselyClipDrawable
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
private val application = EhApplication.application
private val errorDrawable = AppCompatResources.getDrawable(application, R.drawable.image_failed)
open class LoadImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FixedAspectImageView(context, attrs, defStyleAttr), View.OnClickListener,
View.OnLongClickListener {
private var mRequest: ImageRequest? = null
private var mOffsetX = Integer.MIN_VALUE
private var mOffsetY = Integer.MIN_VALUE
private var mClipWidth = Integer.MIN_VALUE
private var mClipHeight = Integer.MIN_VALUE
private var mRetryType: Int = 0
private var mFailed: Boolean = false
private var task: Disposable? = null
private val imageDrawable: Drawable?
get() {
var drawable = drawable
if (drawable is TransitionDrawable) {
if (drawable.numberOfLayers == 2) {
drawable = drawable.getDrawable(1)
}
}
if (drawable is PreciselyClipDrawable) {
drawable = drawable.wrappedDrawable
}
return drawable
private var mUrl: String? = null
@RetryType
private val mRetryType: Int =
context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0).run {
getInt(R.styleable.LoadImageView_retryType, 0).also { recycle() }
}
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0)
setRetryType(a.getInt(R.styleable.LoadImageView_retryType, 0))
a.recycle()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (mFailed) onFailure()
else task?.run { if (isDisposed) reload() }
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
task?.dispose()
clearDrawable()
}
private fun clearDrawable() {
setImageDrawable(null)
}
private fun clearRetry() {
if (mRetryType == RETRY_TYPE_CLICK) {
setOnClickListener(null)
isClickable = false
} else if (mRetryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(null)
isLongClickable = false
}
}
private fun setRetryType(@RetryType retryType: Int) {
if (mRetryType != retryType) {
val oldRetryType = mRetryType
mRetryType = retryType
if (mFailed) {
if (oldRetryType == RETRY_TYPE_CLICK) {
setOnClickListener(null)
isClickable = false
} else if (oldRetryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(null)
isLongClickable = false
}
if (retryType == RETRY_TYPE_CLICK) {
setOnClickListener(this)
} else if (retryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(this)
}
private fun setRetry(canRetry: Boolean) {
when (mRetryType) {
RETRY_TYPE_CLICK -> {
setOnClickListener(if (canRetry) this else null)
isClickable = canRetry
}
RETRY_TYPE_LONG_CLICK -> {
setOnLongClickListener(if (canRetry) this else null)
isLongClickable = canRetry
}
RETRY_TYPE_NONE -> {}
}
}
@@ -127,76 +77,44 @@ open class LoadImageView @JvmOverloads constructor(
mClipHeight = Integer.MIN_VALUE
}
@JvmOverloads
fun load(url: String, isLoaded: Boolean = false) {
if ((mRequest?.data as? String?) == url)
return
mFailed = false
clearRetry()
task?.dispose()
mRequest =
ImageRequest.Builder(context).data(url).crossfade(false)
.size(Size.ORIGINAL).target(
{ onWait() },
{ onFailure() },
{ onGetValue(it, isLoaded) }
).build().also { load(it) }
}
fun load(request: ImageRequest) {
task = context.imageLoader.enqueue(request)
fun load(url: String, crossfade: Boolean = true) {
mUrl = url
load(url) {
size(Size.ORIGINAL)
if (!crossfade) crossfade(false)
listener(
{ setRetry(false) },
{ setRetry(true) },
{ _, _ ->
super.setImageDrawable(errorDrawable)
setRetry(true)
},
{ _, _ -> setRetry(false) }
)
}
}
private fun reload() {
mRequest?.let { load(it) }
mUrl?.let { this.load(it) }
}
private fun onWait() {
mFailed = false
clearDrawable()
override fun setImageDrawable(drawable: Drawable?) {
var newDrawable = drawable
if (newDrawable != null) {
if (Integer.MIN_VALUE != mOffsetX) {
newDrawable = PreciselyClipDrawable(newDrawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
}
onPreSetImageDrawable(newDrawable, true)
}
super.setImageDrawable(newDrawable)
}
private fun onGetValue(drawable: Drawable, isLoaded: Boolean) {
var drawable = drawable
clearDrawable()
if (Integer.MIN_VALUE != mOffsetX) {
drawable = PreciselyClipDrawable(drawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
}
onPreSetImageDrawable(drawable, true)
if (isShown && !isLoaded) {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ColorDrawable(Color.TRANSPARENT)
layers[1] = drawable
val transitionDrawable = TransitionDrawable(layers)
setImageDrawable(transitionDrawable)
transitionDrawable.startTransition(300)
} else {
setImageDrawable(drawable)
}
}
fun onFailure() {
mFailed = true
clearDrawable()
val drawable = ContextCompat.getDrawable(context, R.drawable.image_failed)
onPreSetImageDrawable(drawable, true)
setImageDrawable(drawable)
when (mRetryType) {
RETRY_TYPE_CLICK -> {
setOnClickListener(this)
}
RETRY_TYPE_LONG_CLICK -> {
setOnLongClickListener(this)
}
else -> {
mRequest = null
}
override fun getDrawable(): Drawable? {
var newDrawable = super.getDrawable()
if (newDrawable is PreciselyClipDrawable) {
newDrawable = newDrawable.drawable
}
return newDrawable
}
override fun onClick(v: View) {
@@ -210,8 +128,6 @@ open class LoadImageView @JvmOverloads constructor(
open fun onPreSetImageDrawable(drawable: Drawable?, isTarget: Boolean) {}
open fun onPreSetImageResource(resId: Int, isTarget: Boolean) {}
@IntDef(RETRY_TYPE_NONE, RETRY_TYPE_CLICK, RETRY_TYPE_LONG_CLICK)
@Retention(AnnotationRetention.SOURCE)
private annotation class RetryType
@@ -1,24 +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.yorozuya;
public class AssertError extends Error {
public AssertError(String detailMessage) {
super(detailMessage);
}
}
@@ -1,24 +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.yorozuya;
public class AssertException extends Exception {
public AssertException(String detailMessage) {
super(detailMessage);
}
}
@@ -1,122 +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.yorozuya;
public final class AssertUtils {
private AssertUtils() {
}
public static void assertTrue(boolean cond) {
assertTrue("Condition has to be true.", cond);
}
public static void assertTrueEx(boolean cond) throws AssertException {
assertTrueEx("Condition has to be true.", cond);
}
public static void assertTrue(String message, boolean cond) {
if (!cond) {
throw new AssertError(message);
}
}
public static void assertTrueEx(String message, boolean cond) throws AssertException {
if (!cond) {
throw new AssertException(message);
}
}
public static void assertNull(Object object) {
assertNull("Should be null", object);
}
public static void assertNullEx(Object object) throws AssertException {
assertNullEx("Should be null", object);
}
public static void assertNull(String message, Object object) {
if (object != null) {
throw new AssertError(message);
}
}
public static void assertNullEx(String message, Object object) throws AssertException {
if (object != null) {
throw new AssertException(message);
}
}
public static void assertNotNull(Object object) {
assertNotNull("Should not be null", object);
}
public static void assertNotNullEx(Object object) throws AssertException {
assertNotNullEx("Should not be null", object);
}
public static void assertNotNull(String message, Object object) {
if (object == null) {
throw new AssertError(message);
}
}
public static void assertNotNullEx(String message, Object object) throws AssertException {
if (object == null) {
throw new AssertException(message);
}
}
public static void assertEquals(int expected, int actual) {
assertEquals("Should be " + expected + ", but it is " + actual, expected, actual);
}
public static void assertEqualsEx(int expected, int actual) throws AssertException {
assertEqualsEx("Should be " + expected + ", but it is " + actual, expected, actual);
}
public static void assertEquals(String message, int expected, int actual) {
if (expected != actual) {
throw new AssertError(message);
}
}
public static void assertEqualsEx(String message, int expected, int actual) throws AssertException {
if (expected != actual) {
throw new AssertException(message);
}
}
public static void assertInstanceOf(Object obj, Class clazz) {
assertInstanceOf("The object should be instance of " + clazz.getName(), obj, clazz);
}
public static void assertInstanceOfEx(Object obj, Class clazz) throws AssertException {
assertInstanceOfEx("The object should be instance of " + clazz.getName(), obj, clazz);
}
public static void assertInstanceOf(String message, Object obj, Class clazz) {
if (!clazz.isInstance(obj)) {
throw new AssertError(message);
}
}
public static void assertInstanceOfEx(String message, Object obj, Class clazz) throws AssertException {
if (!clazz.isInstance(obj)) {
throw new AssertException(message);
}
}
}
@@ -0,0 +1,5 @@
package com.hippo.yorozuya
fun assertEquals(expected: Int, actual: Int) {
check(expected == actual)
}
@@ -0,0 +1,19 @@
package com.hippo.yorozuya
import okhttp3.ResponseBody
import java.io.File
fun ResponseBody.copyToFile(file: File) {
val os = file.outputStream()
val channel = os.channel
val source = source()
try {
channel.transferFrom(source, 0, Long.MAX_VALUE)
} catch (e: Throwable) {
throw e
} finally {
source.close()
channel.close()
os.close()
}
}
@@ -18,6 +18,8 @@ package com.hippo.yorozuya;
import android.text.TextUtils;
import org.jsoup.parser.Parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -30,25 +32,6 @@ public final class StringUtils {
'\u00A0', // NO-BREAK SPACE
'\u3000', // IDEOGRAPHIC SPACE
};
private static final String[] ESCAPE_CHARATER_LIST = {
"&amp;",
"&lt;",
"&gt;",
"&quot;",
"&#039;",
"&times;",
"&nbsp;"
};
private static final String[] UNESCAPE_CHARATER_LIST = {
"&",
"<",
">",
"\"",
"'",
"×",
" "
};
private StringUtils() {
}
@@ -57,7 +40,7 @@ public final class StringUtils {
* Unescape xml. It do not work perfectly.
*/
public static String unescapeXml(String str) {
return replaceEach(str, ESCAPE_CHARATER_LIST, UNESCAPE_CHARATER_LIST);
return Parser.unescapeEntities(str, true);
}
/**
@@ -141,218 +124,6 @@ public final class StringUtils {
return buf.toString();
}
/**
* <p>
* Replaces all occurrences of Strings within another String.
* </p>
*
* <p>
* A {@code null} reference passed to this method is a no-op, or if
* any "search string" or "string to replace" is null, that replace will be
* ignored. This will not repeat. For repeating replaces, call the
* overloaded method.
* </p>
*
* <pre>
* StringUtils.replaceEach(null, *, *) = null
* StringUtils.replaceEach("", *, *) = ""
* StringUtils.replaceEach("aba", null, null) = "aba"
* StringUtils.replaceEach("aba", new String[0], null) = "aba"
* StringUtils.replaceEach("aba", null, new String[0]) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, null) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}) = "b"
* StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}) = "aba"
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}) = "wcte"
* (example of how it does not repeat)
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}) = "dcte"
* </pre>
*
* @param text text to search and replace in, no-op if null
* @param searchList the Strings to search for, no-op if null
* @param replacementList the Strings to replace them with, no-op if null
* @return the text with any replacements processed, {@code null} if
* null String input
* @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok,
* and/or size 0)
*/
// Get from org.apache.commons.lang3.StringUtils
public static String replaceEach(final String text, final String[] searchList,
final String[] replacementList) {
return replaceEach(text, searchList, replacementList, false, 0);
}
/**
* <p>
* Replaces all occurrences of Strings within another String.
* </p>
*
* <p>
* A {@code null} reference passed to this method is a no-op, or if
* any "search string" or "string to replace" is null, that replace will be
* ignored.
* </p>
*
* <pre>
* StringUtils.replaceEach(null, *, *, *) = null
* StringUtils.replaceEach("", *, *, *) = ""
* StringUtils.replaceEach("aba", null, null, *) = "aba"
* StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
* StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
* StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
* (example of how it repeats)
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalStateException
* </pre>
*
* @param text text to search and replace in, no-op if null
* @param searchList the Strings to search for, no-op if null
* @param replacementList the Strings to replace them with, no-op if null
* @param repeat if true, then replace repeatedly
* until there are no more possible replacements or timeToLive < 0
* @param timeToLive if less than 0 then there is a circular reference and endless
* loop
* @return the text with any replacements processed, {@code null} if
* null String input
* @throws IllegalStateException if the search is repeating and there is an endless loop due
* to outputs of one being inputs to another
* @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok,
* and/or size 0)
*/
// Get from org.apache.commons.lang3.StringUtils
private static String replaceEach(
final String text, final String[] searchList,
final String[] replacementList, final boolean repeat,
final int timeToLive) {
// mchyzer Performance note: This creates very few new objects (one major goal)
// let me know if there are performance requests, we can create a harness to measure
if (text == null || TextUtils.isEmpty(text) || searchList == null ||
searchList.length == 0 || replacementList == null || replacementList.length == 0) {
return text;
}
// if recursing, this shouldn't be less than 0
if (timeToLive < 0) {
throw new IllegalStateException("Aborting to protect against StackOverflowError - " +
"output of one loop is the input of another");
}
final int searchLength = searchList.length;
final int replacementLength = replacementList.length;
// make sure lengths are ok, these need to be equal
if (searchLength != replacementLength) {
throw new IllegalArgumentException("Search and Replace array lengths don't match: "
+ searchLength
+ " vs "
+ replacementLength);
}
// keep track of which still have matches
final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];
// index on index that the match was found
int textIndex = -1;
int replaceIndex = -1;
int tempIndex = -1;
// index of replace array that will replace the search string found
// NOTE: logic duplicated below START
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
TextUtils.isEmpty(searchList[i]) || replacementList[i] == null) {
continue;
}
tempIndex = text.indexOf(searchList[i]);
// see if we need to keep searching for this
if (tempIndex == -1) {
noMoreMatchesForReplIndex[i] = true;
} else {
if (textIndex == -1 || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
}
// NOTE: logic mostly below END
// no search strings found, we are done
if (textIndex == -1) {
return text;
}
int start = 0;
// get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit
int increase = 0;
// count the replacement text elements that are larger than their corresponding text being replaced
for (int i = 0; i < searchList.length; i++) {
if (searchList[i] == null || replacementList[i] == null) {
continue;
}
final int greater = replacementList[i].length() - searchList[i].length();
if (greater > 0) {
increase += 3 * greater; // assume 3 matches
}
}
// have upper-bound at 20% increase, then let Java take over
increase = Math.min(increase, text.length() / 5);
final StringBuilder buf = new StringBuilder(text.length() + increase);
while (textIndex != -1) {
for (int i = start; i < textIndex; i++) {
buf.append(text.charAt(i));
}
buf.append(replacementList[replaceIndex]);
start = textIndex + searchList[replaceIndex].length();
textIndex = -1;
replaceIndex = -1;
tempIndex = -1;
// find the next earliest match
// NOTE: logic mostly duplicated above START
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
TextUtils.isEmpty(searchList[i]) || replacementList[i] == null) {
continue;
}
tempIndex = text.indexOf(searchList[i], start);
// see if we need to keep searching for this
if (tempIndex == -1) {
noMoreMatchesForReplIndex[i] = true;
} else {
if (textIndex == -1 || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
}
// NOTE: logic duplicated above END
}
final int textLength = text.length();
for (int i = start; i < textLength; i++) {
buf.append(text.charAt(i));
}
final String result = buf.toString();
if (!repeat) {
return result;
}
return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1);
}
public static boolean endsWith(String string, String[] suffixs) {
for (int i = 0, n = suffixs.length; i < n; i++) {
if (string.endsWith(suffixs[i])) {
@@ -464,38 +235,6 @@ public final class StringUtils {
return null == str ? 0 : str.length();
}
/**
* <p>Counts how many times the char appears in the given string.</p>
*
* <p>A {@code null} or empty ("") String input returns {@code 0}.</p>
*
* <pre>
* StringUtils.countMatches(null, *) = 0
* StringUtils.countMatches("", *) = 0
* StringUtils.countMatches("abba", 0) = 0
* StringUtils.countMatches("abba", 'a') = 2
* StringUtils.countMatches("abba", 'b') = 2
* StringUtils.countMatches("abba", 'x') = 0
* </pre>
*
* @param str the CharSequence to check, may be null
* @param ch the char to count
* @return the number of occurrences, 0 if the CharSequence is {@code null}
*/
public static int countMatches(final CharSequence str, final char ch) {
if (TextUtils.isEmpty(str)) {
return 0;
}
int count = 0;
// We could also call str.toCharArray() for faster look ups but that would generate more garbage.
for (int i = 0; i < str.length(); i++) {
if (ch == str.charAt(i)) {
count++;
}
}
return count;
}
/**
* All null or empty, or all not
*/
@@ -1,5 +1,11 @@
package com.hippo.yorozuya
import org.jsoup.parser.Parser
fun String.unescapeXml(): String {
return StringUtils.unescapeXml(this)
return Parser.unescapeEntities(this, true)
}
inline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T {
return block(trim())
}
@@ -1,50 +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.yorozuya.thread;
public class PVLock {
private int mCounter;
public PVLock(int count) {
mCounter = count;
}
/**
* Obtain
*/
public synchronized void p() throws InterruptedException {
while (true) {
if (mCounter > 0) {
mCounter--;
break;
} else {
this.wait();
}
}
}
/**
* Release
*/
public synchronized void v() {
mCounter++;
if (mCounter > 0) {
this.notify();
}
}
}
@@ -1,40 +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.yorozuya.thread;
import android.os.Process;
public class PriorityThread extends Thread {
private final int mPriority;
public PriorityThread(Runnable runnable, int priority) {
super(runnable);
mPriority = priority;
}
public PriorityThread(Runnable runnable, String name, int priority) {
super(runnable, name);
mPriority = priority;
}
@Override
public void run() {
Process.setThreadPriority(mPriority);
super.run();
}
}
@@ -1,42 +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.yorozuya.thread;
import androidx.annotation.NonNull;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A thread factory that creates threads with a given thread priority.
*/
public class PriorityThreadFactory implements ThreadFactory {
private final int mPriority;
private final AtomicInteger mIdGenerator = new AtomicInteger();
private final String mName;
public PriorityThreadFactory(String name, int priority) {
mName = name;
mPriority = priority;
}
@Override
public Thread newThread(@NonNull Runnable r) {
return new PriorityThread(r, mName + "-" + mIdGenerator.getAndIncrement(), mPriority);
}
}
@@ -33,8 +33,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Message
import android.provider.MediaStore
import android.text.TextUtils
import android.view.HapticFeedbackConstants
@@ -80,7 +78,6 @@ import com.hippo.util.getParcelableExtraCompat
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IOUtils
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
@@ -89,7 +86,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
@@ -98,18 +95,22 @@ import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.math.abs
import kotlin.math.max
@@ -164,9 +165,9 @@ class ReaderActivity : EhActivity() {
private var mSize: Int = 0
private var mCurrentIndex: Int = 0
private var mSavingPage = -1
private var builder: EditTextDialogBuilder? = null
private lateinit var builder: EditTextDialogBuilder
private var dialogShown = false
private var dialog: AlertDialog? = null
private lateinit var dialog: AlertDialog
private var requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { result ->
@@ -197,10 +198,7 @@ class ReaderActivity : EhActivity() {
}
if (ACTION_EH == mAction) {
if (mGalleryInfo != null) {
mGalleryProvider =
EhPageLoader(mGalleryInfo)
}
mGalleryInfo?.let { mGalleryProvider = EhPageLoader(it) }
} else if (Intent.ACTION_VIEW == mAction) {
if (mUri != null) {
try {
@@ -213,8 +211,41 @@ class ReaderActivity : EhActivity() {
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
}
mGalleryProvider =
ArchivePageLoader(this, mUri)
val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)
mGalleryProvider = ArchivePageLoader(this, mUri!!,
flow {
if (!dialogShown) {
withUIContext {
dialogShown = true
dialog.run {
show()
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val passwd = builder.text
if (passwd.isEmpty())
builder.setError(getString(R.string.passwd_cannot_be_empty))
else {
continuation.getAndSet(null)?.resume(passwd)
}
}
setOnCancelListener {
finish()
}
}
}
}
while (true) {
currentCoroutineContext().ensureActive()
val r = suspendCancellableCoroutine {
continuation.set(it)
it.invokeOnCancellation { dialog.dismiss() }
}
emit(r)
withUIContext {
builder.setError(getString(R.string.passwd_wrong))
}
}
}
)
}
}
}
@@ -264,29 +295,23 @@ class ReaderActivity : EhActivity() {
binding = ReaderActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))
builder!!.setTitle(getString(R.string.archive_need_passwd))
builder!!.setPositiveButton(getString(android.R.string.ok), null)
dialog = builder!!.create()
dialog!!.setCanceledOnTouchOutside(false)
builder.setTitle(getString(R.string.archive_need_passwd))
builder.setPositiveButton(getString(android.R.string.ok), null)
dialog = builder.create()
dialog.setCanceledOnTouchOutside(false)
if (mGalleryProvider == null) {
finish()
return
}
ArchivePageLoader.showPasswd = ShowPasswdDialogHandler(this)
mGalleryProvider!!.start()
// Get start page
if (savedInstanceState == null) {
mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage
}
lifecycleScope.launchUI {
mGalleryProvider!!.state.collect {
if (it == PageLoader.STATE_READY) {
setGallery()
currentCoroutineContext().cancel()
}
mGalleryProvider.let {
if (it == null) {
finish()
return
}
lifecycleScope.launchIO {
it.start()
if (it.awaitReady()) withUIContext { setGallery() }
}
}
@@ -295,9 +320,14 @@ class ReaderActivity : EhActivity() {
}
fun setGallery() {
mSize = mGalleryProvider!!.size()
val viewerMode =
ReadingModeType.fromPreference(readerPreferences.defaultReadingMode().get())
if (mGalleryProvider?.isReady != true) return
// TODO: Not well place to call it
dialog.dismiss()
// Get start page
if (mCurrentIndex == 0) mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage
mSize = mGalleryProvider!!.size
val viewerMode = ReadingModeType.fromPreference(readerPreferences.defaultReadingMode().get())
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
viewer?.destroy()
viewer = ReadingModeType.toViewer(readerPreferences.defaultReadingMode().get(), this)
@@ -535,56 +565,6 @@ class ReaderActivity : EhActivity() {
}
}
private fun showPasswdDialog() {
if (!dialogShown) {
dialogShown = true
dialog!!.show()
if (dialog!!.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE)
.setOnClickListener { this@ReaderActivity.onProvidePasswd() }
}
dialog!!.setOnCancelListener { finish() }
}
}
private fun onProvidePasswd() {
val passwd = builder!!.text
if (passwd.isEmpty())
builder!!.setError(getString(R.string.passwd_cannot_be_empty))
else {
ArchivePageLoader.passwd = passwd
ArchivePageLoader.pv.v()
}
}
private fun onPasswdWrong() {
builder!!.setError(getString(R.string.passwd_wrong))
}
private fun onPasswdCorrect() {
dialog!!.dismiss()
}
private class ShowPasswdDialogHandler(activity: ReaderActivity) : Handler() {
private val weakReference: WeakReference<ReaderActivity>
init {
this.weakReference = WeakReference(activity)
}
override fun handleMessage(msg: Message) {
val activity = weakReference.get()
super.handleMessage(msg)
if (null != activity) {
when (msg.what) {
0 -> activity.showPasswdDialog()
1 -> activity.onPasswdWrong()
2 -> activity.onPasswdCorrect()
}
}
}
}
companion object {
const val ACTION_EH = "eh"
const val KEY_ACTION = "action"
@@ -735,12 +715,13 @@ class ReaderActivity : EhActivity() {
readerPreferences.defaultReadingMode().set(newReadingMode.flagValue)
setGallery()
updateCropBordersShortcut()
// updateCropBordersShortcut()
}
}
}
// Crop borders
/*
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
@@ -761,6 +742,7 @@ class ReaderActivity : EhActivity() {
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
*/
// Rotation
with(binding.actionRotation) {
@@ -801,6 +783,7 @@ class ReaderActivity : EhActivity() {
binding.actionRotation.setImageResource(orientation.iconRes)
}
/*
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get())
val enabled = if (isPagerType) {
@@ -817,6 +800,7 @@ class ReaderActivity : EhActivity() {
},
)
}
*/
/**
@@ -61,7 +61,7 @@ class ReaderPageSheet(
}
private fun refresh() {
activity.mGalleryProvider?.retryPage(page)
activity.mGalleryProvider?.retryPage(page.index)
dismiss()
}
}
@@ -7,21 +7,19 @@ import com.hippo.yorozuya.MathUtils
import com.hippo.yorozuya.OSUtils
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import kotlinx.coroutines.flow.MutableStateFlow
abstract class PageLoader {
private val mImageCache = ImageCache()
val mPages by lazy {
check(size() > 0)
(0 until size()).map { ReaderPage(it) }
check(size > 0)
(0 until size).map { ReaderPage(it) }
}
private val mPreloads = MathUtils.clamp(com.hippo.ehviewer.Settings.getPreloadImage(), 0, 100)
var state = MutableStateFlow(STATE_WAIT)
abstract val error: String
abstract suspend fun awaitReady(): Boolean
abstract val isReady: Boolean
abstract fun start()
@@ -34,25 +32,31 @@ abstract class PageLoader {
mImageCache.evictAll()
}
abstract fun size(): Int
abstract val size: Int
fun request(page: ReaderPage) {
val index = mPages.indexOf(page)
fun request(index: Int) {
val image = mImageCache[index]
if (image != null)
if (image != null) {
notifyPageSucceed(index, image)
else
} else {
notifyPageWait(index)
onRequest(index)
}
// val pagesAbsent = (index until (mPreloads + index).coerceAtMost(size())).toMutableList().removeAll(mImageCache.snapshot().keys)
// Should we refresh our LruCache ?
val pagesAbsent = ((index - 5).coerceAtLeast(0) until (mPreloads + index).coerceAtMost(size())).mapNotNull { it.takeIf { mImageCache[it] == null } }
preloadPages(pagesAbsent, (index - 10).coerceAtLeast(0) to (mPreloads + index + 10).coerceAtMost(size()))
val pagesAbsent =
((index - 5).coerceAtLeast(0) until (mPreloads + index).coerceAtMost(size)).mapNotNull { it.takeIf { mImageCache[it] == null } }
preloadPages(
pagesAbsent, (index - 10).coerceAtLeast(0) to (mPreloads + index + 10).coerceAtMost(
size
)
)
}
fun retryPage(page: ReaderPage) {
page.status.value = Page.State.QUEUE
onForceRequest(mPages.indexOf(page))
fun retryPage(index: Int) {
notifyPageWait(index)
onForceRequest(index)
}
protected abstract fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>)
@@ -61,19 +65,12 @@ abstract class PageLoader {
protected abstract fun onForceRequest(index: Int)
fun cancelRequest(page: ReaderPage) {
onCancelRequest(mPages.indexOf(page))
fun cancelRequest(index: Int) {
onCancelRequest(index)
}
protected abstract fun onCancelRequest(index: Int)
fun notifyDataChanged() {
if (size() == STATE_ERROR)
state.value = STATE_ERROR
else
state.compareAndSet(STATE_WAIT, STATE_READY)
}
fun notifyPageWait(index: Int) {
mPages[index].status.value = Page.State.QUEUE
}
@@ -95,7 +92,13 @@ abstract class PageLoader {
mPages[index].status.value = Page.State.ERROR
}
private class ImageCache : LruCache<Int, Image>(MathUtils.clamp(OSUtils.getTotalMemory() / 16, MIN_CACHE_SIZE, MAX_CACHE_SIZE).toInt()) {
private class ImageCache : LruCache<Int, Image>(
MathUtils.clamp(
OSUtils.getTotalMemory() / 16,
MIN_CACHE_SIZE,
MAX_CACHE_SIZE
).toInt()
) {
fun add(key: Int, value: Image) {
put(key, value)
}
@@ -113,10 +116,4 @@ abstract class PageLoader {
private const val MIN_CACHE_SIZE = (128 * 1024 * 1024).toLong()
}
}
companion object {
const val STATE_WAIT = -1
const val STATE_ERROR = -2
const val STATE_READY = -3
}
}
@@ -100,7 +100,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(
binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom())
binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1)
binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders())
// binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders())
}
/**
@@ -119,7 +119,7 @@ class ReaderReadingModeSettings @JvmOverloads constructor(
readerPreferences.navigationModeWebtoon()
.asHotFlow { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 }
.launchIn((context as ReaderActivity).lifecycleScope)
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon())
// binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon())
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(
readerPreferences.webtoonSidePadding(),
R.array.webtoon_side_padding_values
@@ -18,10 +18,6 @@ import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import coil.dispose
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.EASE_OUT_QUAD
@@ -30,8 +26,6 @@ import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import java.io.InputStream
import java.nio.ByteBuffer
/**
* A wrapper view for showing page image.
@@ -137,21 +131,9 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
}
fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
this.config = config
if (isAnimated) {
prepareAnimatedImageView()
setAnimatedImage(inputStream, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config)
}
}
fun recycle() = pageView?.let {
when (it) {
is SubsamplingScaleImageView -> it.recycle()
is AppCompatImageView -> it.dispose()
}
it.isVisible = false
}
@@ -252,7 +234,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
private fun setNonAnimatedImage(
image: Any,
image: Drawable,
config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
@@ -273,14 +255,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
},
)
when (image) {
is Drawable -> {
val bitmap = (image as BitmapDrawable).bitmap
setImage(ImageSource.cachedBitmap(bitmap))
}
is InputStream -> setImage(ImageSource.inputStream(image))
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
val bitmap = (image as BitmapDrawable).bitmap
setImage(ImageSource.cachedBitmap(bitmap))
isVisible = true
}
@@ -324,36 +300,17 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
private fun setAnimatedImage(
image: Any,
image: Drawable,
config: Config,
) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) {
setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
}
val data = when (image) {
is Drawable -> image
is InputStream -> ByteBuffer.wrap(image.readBytes())
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
val request = ImageRequest.Builder(context)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
setImageDrawable(result)
(result as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
},
onError = {
this@ReaderPageImageView.onImageLoadError()
},
)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
setImageDrawable(image)
(image as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
}
private fun Int.getSystemScaledDuration(): Int {
@@ -63,8 +63,8 @@ class PagerConfig(
readerPreferences.zoomStart()
.register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() })
readerPreferences.cropBorders()
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
// readerPreferences.cropBorders()
// .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.navigateToPan()
.register({ navigateToPan = it })
@@ -202,7 +202,7 @@ class PagerPageHolder(
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
errorLayout?.actionRetry?.viewer = viewer
errorLayout?.actionRetry?.setOnClickListener {
viewer.activity.mGalleryProvider?.retryPage(page)
viewer.activity.mGalleryProvider?.retryPage(page.index)
}
}
page.errorMsg?.let { errorLayout!!.errorMessage.text = it }
@@ -9,17 +9,17 @@ import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
/**
* Pager adapter used by this [viewer] to where [ViewerChapters] updates are posted.
* Pager adapter used by this [viewer] to where [PageLoader] updates are posted.
*/
class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/**
* List of currently set items.
*/
var items: MutableList<Any> = mutableListOf()
var items: List<ReaderPage> = emptyList()
private set
var currentChapter: PageLoader? = null
private var currentChapter: PageLoader? = null
/**
* Context that has been wrapped to use the correct theme values based on the
@@ -33,11 +33,11 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
* has R2L direction.
*/
fun setChapters(chapters: PageLoader) {
items = chapters.mPages.toMutableList()
items = chapters.mPages
currentChapter = chapters
if (viewer is R2LPagerViewer) {
items.reverse()
items = items.asReversed()
}
notifyDataSetChanged()
@@ -54,16 +54,14 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
* Creates a new view for the item at the given [position].
*/
override fun createView(container: ViewGroup, position: Int): View {
currentChapter?.request(items[position] as ReaderPage)
return when (val item = items[position]) {
is ReaderPage -> PagerPageHolder(readerThemedContext, viewer, item)
else -> throw NotImplementedError("Holder for ${item.javaClass} not implemented")
}
val item = items[position]
currentChapter?.request(item.index)
return PagerPageHolder(readerThemedContext, viewer, item)
}
override fun destroyView(container: ViewGroup, position: Int, view: View) {
val item = items[position]
currentChapter?.cancelRequest(item as ReaderPage)
currentChapter?.cancelRequest(item.index)
}
/**
@@ -8,17 +8,17 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
/**
* RecyclerView Adapter used by this [viewer] to where [ViewerChapters] updates are posted.
* RecyclerView Adapter used by this [viewer] to where [PageLoader] updates are posted.
*/
class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/**
* List of currently set items.
*/
var items: List<Any> = emptyList()
var items: List<ReaderPage> = emptyList()
private set
var currentChapter: PageLoader? = null
private var currentChapter: PageLoader? = null
/**
* Context that has been wrapped to use the correct theme values based on the
@@ -46,27 +46,12 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
return items.size
}
/**
* Returns the view type for the item at the given [position].
*/
override fun getItemViewType(position: Int): Int {
return when (val item = items[position]) {
is ReaderPage -> PAGE_VIEW
else -> error("Unknown view type for ${item.javaClass}")
}
}
/**
* Creates a new view holder for an item with the given [viewType].
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
PAGE_VIEW -> {
val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
WebtoonPageHolder(view, viewer)
}
else -> error("Unknown view type")
}
val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
return WebtoonPageHolder(view, viewer)
}
/**
@@ -74,7 +59,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
*/
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = items[position]
currentChapter!!.request(item as ReaderPage)
currentChapter!!.request(position)
when (holder) {
is WebtoonPageHolder -> holder.bind(item)
}
@@ -84,15 +69,9 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
* Recycles an existing view [holder] before adding it to the view pool.
*/
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
currentChapter?.cancelRequest(holder.bindingAdapterPosition)
when (holder) {
is WebtoonPageHolder -> holder.recycle()
}
val item = items.getOrNull(holder.bindingAdapterPosition) ?: return
currentChapter?.cancelRequest(item as ReaderPage)
}
}
/**
* View holder type of a chapter page view.
*/
private const val PAGE_VIEW = 0
@@ -39,8 +39,8 @@ class WebtoonConfig(
val theme = readerPreferences.readerTheme().get()
init {
readerPreferences.cropBordersWebtoon()
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
// readerPreferences.cropBordersWebtoon()
// .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
readerPreferences.webtoonSidePadding()
.register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
@@ -252,7 +252,7 @@ class WebtoonPageHolder(
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, defaultHeight)
errorLayout?.actionRetry?.setOnClickListener {
viewer.activity.mGalleryProvider?.retryPage(page!!)
viewer.activity.mGalleryProvider?.retryPage(page!!.index)
}
}
page?.errorMsg?.let { errorLayout!!.errorMessage.text = it }
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more