Compare commits

..
13 Commits
Author SHA1 Message Date
tarsin ce823e9b20 Release 1.8.6.2
Releases / Build (push) Has been cancelled
2023-02-20 19:06:59 +08:00
FooIbar 0caa5b1a3b Fix 509 detection false positive 2023-02-20 19:06:13 +08:00
FooIbar 9edea5d584 Clean up unfinished download files 2023-02-20 18:48:49 +08:00
FooIbar 9a773aeb39 Workaround for del tag parsing in comments 2023-02-20 18:48:40 +08:00
FooIbar 9fd3196bb1 Use initializer instead of getter for coroutineContext 2023-02-20 18:48:34 +08:00
FooIbar 7580572874 Temporarily disable vpn bypass on login 2023-02-20 18:48:27 +08:00
tarsin 06acc620f4 Resolve #842 2023-02-20 18:48:19 +08:00
FooIbar 367358d277 Resolve #789 2023-02-20 18:48:13 +08:00
tarsin d61df599e8 Release 1.8.6.1
Releases / Build (push) Has been cancelled
2023-02-18 10:38:33 +08:00
tarsin 989a0a01c1 Fix quicksearch progress 2023-02-18 10:37:57 +08:00
tarsin 12108645e9 Fix a stupid memleak 2023-02-18 10:37:45 +08:00
FooIbar 4910ec194d Fix search keyword in tag mode 2023-02-18 10:37:29 +08:00
FooIbar f730f43945 Fix comment last edited time parsing 2023-02-18 10:37:14 +08:00
128 changed files with 5077 additions and 3438 deletions

No files matched your search

+6
View File
@@ -1,3 +1,9 @@
[submodule "app/src/main/cpp/libarchive"]
path = app/src/main/cpp/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "app/src/main/cpp/liblzma"]
path = app/src/main/cpp/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+13 -31
View File
@@ -1,8 +1,3 @@
import java.io.ByteArrayOutputStream
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
plugins {
id("com.android.application")
kotlin("android")
@@ -13,7 +8,7 @@ plugins {
android {
compileSdk = 33
buildToolsVersion = "33.0.2"
buildToolsVersion = "33.0.1"
ndkVersion = "25.2.9519653"
splits {
@@ -35,26 +30,12 @@ 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 = 180035
versionName = "1.8.7.0"
versionCode = 180034
versionName = "1.8.6.2"
resourceConfigurations.addAll(
listOf(
"zh",
@@ -71,8 +52,6 @@ android {
"nb-rNO"
)
)
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
}
externalNativeBuild {
@@ -93,12 +72,15 @@ 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=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
@@ -143,7 +125,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.3"
kotlinCompilerExtensionVersion = "1.4.2"
}
namespace = "com.hippo.ehviewer"
@@ -162,7 +144,7 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
implementation("androidx.core:core-ktx:1.10.0-beta01")
implementation("androidx.core:core-ktx:1.10.0-alpha02")
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
@@ -187,7 +169,6 @@ 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
@@ -208,11 +189,12 @@ 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-compose:2.2.2")
// implementation 'io.coil-kt:coil:2.2.2'
implementation("com.github.Ehviewer-Overhauled.coil:coil-compose:53302f2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0")
implementation("org.jsoup:jsoup:1.15.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0-RC")
implementation("org.jsoup:jsoup:1.15.3")
}
configurations.all {
@@ -1,34 +0,0 @@
From fc9fb0ea245259e346ab46c334f03b557fe63b6b Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Tue, 22 Nov 2022 12:37:12 +0800
Subject: [PATCH] Don't build other binarys
---
CMakeLists.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 01c73cd..b461e68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -592,7 +592,7 @@ check_symbol_exists(getopt_long getopt.h HAVE_GETOPT_LONG)
# xzdec
#############################################################################
-if(HAVE_GETOPT_LONG)
+if(0)
add_executable(xzdec
src/common/sysdefs.h
src/common/tuklib_common.h
@@ -630,7 +630,7 @@ endif()
# xz
#############################################################################
-if(NOT MSVC AND HAVE_GETOPT_LONG)
+if(0)
add_executable(xz
src/common/mythread.h
src/common/sysdefs.h
--
2.39.2
@@ -1,38 +0,0 @@
From 2a67861e6f06794f3671c4988bac12330d8422e3 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Fri, 27 May 2022 23:21:20 +0800
Subject: [PATCH 1/2] Don't build shared lib
---
libarchive/CMakeLists.txt | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt
index ff7ade00..00c2e170 100644
--- a/libarchive/CMakeLists.txt
+++ b/libarchive/CMakeLists.txt
@@ -242,12 +242,6 @@ ELSEIF(ARCHIVE_ACL_SUNOS)
LIST(APPEND libarchive_SOURCES archive_disk_acl_sunos.c)
ENDIF()
-# Libarchive is a shared library
-ADD_LIBRARY(archive SHARED ${libarchive_SOURCES} ${include_HEADERS})
-TARGET_INCLUDE_DIRECTORIES(archive PUBLIC .)
-TARGET_LINK_LIBRARIES(archive ${ADDITIONAL_LIBS})
-SET_TARGET_PROPERTIES(archive PROPERTIES SOVERSION ${SOVERSION})
-
# archive_static is a static library
ADD_LIBRARY(archive_static STATIC ${libarchive_SOURCES} ${include_HEADERS})
TARGET_LINK_LIBRARIES(archive_static ${ADDITIONAL_LIBS})
@@ -260,7 +254,7 @@ ENDIF(NOT WIN32 OR CYGWIN)
IF(ENABLE_INSTALL)
# How to install the libraries
- INSTALL(TARGETS archive archive_static
+ INSTALL(TARGETS archive_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
--
2.39.2
@@ -1,25 +0,0 @@
From c9298fd6dc0a8b04dd4b2247118b9cb895c46e58 Mon Sep 17 00:00:00 2001
From: tarsin <yuanqingxiang233@163.com>
Date: Sat, 23 Jul 2022 16:05:36 +0800
Subject: [PATCH 2/2] Insert link libs
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 713e3bc5..7247640d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -415,7 +415,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
# # endif
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
-SET(ADDITIONAL_LIBS "")
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
#
# Find ZLIB
#
--
2.39.2
+13 -30
View File
@@ -1,9 +1,7 @@
cmake_minimum_required(VERSION 3.14)
project(ehviewer C)
include(FetchContent)
project(ehviewer)
cmake_minimum_required(VERSION 3.4.1)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
-mllvm -polly \
-mllvm -polly-run-dce \
-mllvm -polly-run-inliner \
@@ -14,20 +12,6 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
-mllvm -polly-vectorizer=stripmine \
-mllvm -polly-detect-profitability-min-per-loop-insts=40 \
-mllvm -polly-invariant-load-hoisting")
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
set(FETCHCONTENT_BASE_DIR ${deps})
FetchContent_Declare(liblzma
GIT_REPOSITORY https://git.tukaani.org/xz.git
GIT_TAG v5.4.1
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binarys.patch || true
)
FetchContent_MakeAvailable(liblzma)
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
# Build GNUTLS libnettle
add_subdirectory(nettle)
@@ -43,7 +27,11 @@ set(HAVE_NETTLE_PBKDF2_H 1)
set(HAVE_NETTLE_RIPEMD160_H 1)
set(HAVE_NETTLE_SHA_H 1)
# Build liblzma
add_subdirectory(liblzma)
# Configure lzma support for libarchive
include_directories(liblzma/src/liblzma/api)
SET(HAVE_LIBLZMA 1)
SET(HAVE_LZMA_H 1)
SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
@@ -51,17 +39,12 @@ SET(HAVE_LZMADEC_H 1)
SET(HAVE_LIBLZMADEC 1)
# Configure libarchive link's static lib
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
FetchContent_Declare(libarchive
GIT_REPOSITORY https://github.com/libarchive/libarchive
GIT_TAG v3.6.2
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-shared-lib.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || true
)
FetchContent_MakeAvailable(libarchive)
include_directories(${libarchive_SOURCE_DIR}/libarchive)
# Build libarchive
add_subdirectory(libarchive)
# Build and link our app's native lib
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c natsort/strnatcmp.c)
target_link_libraries(${PROJECT_NAME} archive_static log)
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c)
target_include_directories(${PROJECT_NAME} PUBLIC libarchive/libarchive)
target_link_libraries(${PROJECT_NAME} archive_static log -Wl,--gc-sections)
+33 -95
View File
@@ -17,12 +17,14 @@
* 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>
@@ -32,7 +34,6 @@
#define LOG_TAG "libarchive_wrapper"
#include "natsort/strnatcmp.h"
#include "ehviewer.h"
typedef struct {
@@ -45,25 +46,12 @@ 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;
@@ -71,6 +59,8 @@ 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",
@@ -97,7 +87,9 @@ 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;
return strnatcmp(fa, fb);
int a_len = (int) strlen(fa);
int b_len = (int) strlen(fb);
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
}
static long archive_map_entries_index(archive_ctx *ctx) {
@@ -107,7 +99,6 @@ 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++;
}
}
@@ -117,48 +108,6 @@ 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)
@@ -209,6 +158,7 @@ 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;
@@ -283,6 +233,8 @@ 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));
@@ -303,7 +255,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%zu%s", "Found ", entryCount, " image entries in archive");
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
@@ -340,52 +292,41 @@ 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);
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);
index = entries[index].index;
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
long long size = archive_entry_size(ctx->entry);
void *buffer = malloc(size);
if (!buffer) {
ctx->using = 0;
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);
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
}
ret = archive_read_data(ctx->arc, buffer, size);
ctx->using = 0;
if (ret == size)
return (*env)->NewDirectByteBuffer(env, buffer, size);
if (ret != size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(buffer);
return 0;
}
#pragma clang diagnostic pop
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
EH_UNUSED(env);
@@ -398,8 +339,6 @@ 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);
}
@@ -476,6 +415,5 @@ JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
EH_UNUSED(thiz);
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
mempool_release_pages(addr, size);
}
free(addr);
}
-9
View File
@@ -1,7 +1,5 @@
#include <jni.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <android/log.h>
#include <string.h>
@@ -27,10 +25,3 @@ 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
@@ -1,177 +0,0 @@
/* -*- 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
@@ -1,38 +0,0 @@
/* -*- 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,8 +1,15 @@
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
@@ -76,6 +83,38 @@ 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.
*
@@ -86,6 +125,16 @@ 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,9 +19,12 @@ 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
@@ -300,6 +303,42 @@ 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)
}
@@ -1914,20 +1953,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,8 +4,10 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.lang.Exception
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
@@ -0,0 +1,11 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
private val assets = context.assets
@Throws(IOException::class)
override fun openStream() = assets.open(assetName)
}
@@ -1,3 +1,10 @@
package com.davemorrissey.labs.subscaleview.provider
interface InputProvider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
interface InputProvider {
@Throws(IOException::class)
fun openStream(): InputStream?
}
@@ -0,0 +1,16 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
/**
* Provider from an already open input stream.
*
*
* Please keep in mind this provider is not reusable.
*/
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
@Throws(IOException::class)
override fun openStream() = stream
}
@@ -0,0 +1,11 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
private val resources = context.resources
@Throws(IOException::class)
override fun openStream() = resources.openRawResource(resource)
}
@@ -0,0 +1,13 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import android.net.Uri
import java.io.IOException
import kotlin.Throws
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
private val resolver = context.contentResolver
@Throws(IOException::class)
override fun openStream() = resolver.openInputStream(uri)
}
-19
View File
@@ -17,7 +17,6 @@
*/
package com.hippo
import android.os.ParcelFileDescriptor
import java.io.FileDescriptor
object Native {
@@ -27,22 +26,4 @@ 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,14 +20,12 @@ 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, private val uri: Uri) {
class UriArchiveAccessor(ctx: Context, 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)
}
@@ -41,7 +39,6 @@ class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
fun close() {
closeArchive()
pfd.close()
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
}
fun getImageSource(index: Int): Image.CloseableSource? {
@@ -64,6 +61,4 @@ class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
@JvmStatic
private external fun releaseByteBuffer(buffer: ByteBuffer)
}
}
private const val DEBUG_TAG = "UriArchiveAccessor"
}
@@ -0,0 +1,190 @@
/*
* 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);
}
}
}
@@ -0,0 +1,104 @@
/*
* 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);
}
}
}
@@ -1,68 +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 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) }
}
}
@@ -0,0 +1,202 @@
/*
* 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);
}
}
}
@@ -1,173 +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.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,23 +18,31 @@ package com.hippo.ehviewer
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.Application
import android.content.ComponentCallbacks2
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.util.DebugLogger
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.coil.MergeInterceptor
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.dao.buildMainDB
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.ui.CommonOperations
@@ -47,6 +55,8 @@ 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
@@ -77,7 +87,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(e)
Crash.saveCrashLog(application, e)
}
} catch (ignored: Throwable) {
}
@@ -96,11 +106,14 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
ehDatabase
}
launchIO {
DownloadManager.isIdle
downloadManager
}
launchIO {
cleanupDownload()
}
launchIO {
theDawnOfNewDay()
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
}
@@ -127,6 +140,54 @@ 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) {
@@ -182,12 +243,16 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
locked = true
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
okHttpClient(okHttpClient)
components { add(MergeInterceptor) }
crossfade(300)
if (BuildConfig.DEBUG) logger(DebugLogger())
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)
}.build()
}
@@ -235,6 +300,9 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
}
}
@JvmStatic
val downloadManager by lazy { DownloadManager }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@@ -0,0 +1,591 @@
/*
* 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);
}
}
}
@@ -1,459 +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.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.AssertUtilsKt;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.NumberUtils;
import java.util.Locale;
@@ -63,9 +63,6 @@ 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,
@@ -74,7 +71,6 @@ public class Settings {
KEY_MY_TAGS,
KEY_SHOW_JPN_TITLE,
KEY_REQUEST_NEWS,
KEY_REQUEST_NEWS_TIMER,
KEY_HIDE_HV_EVENTS
};
/********************
@@ -107,7 +103,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 = false;
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String TAG = Settings.class.getSimpleName();
@@ -209,7 +205,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 = false;
private static final boolean DEFAULT_REQUEST_NEWS = true;
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;
@@ -566,7 +562,7 @@ public class Settings {
}
public static void putFavCat(String[] value) {
AssertUtilsKt.assertEquals(10, value.length);
AssertUtils.assertEquals(10, value.length);
sSettingsPre.edit()
.putString(KEY_FAV_CAT_0, value[0])
.putString(KEY_FAV_CAT_1, value[1])
@@ -597,7 +593,7 @@ public class Settings {
}
public static void putFavCount(int[] count) {
AssertUtilsKt.assertEquals(10, count.length);
AssertUtils.assertEquals(10, count.length);
sSettingsPre.edit()
.putInt(KEY_FAV_COUNT_0, count[0])
.putInt(KEY_FAV_COUNT_1, count[1])
@@ -183,10 +183,7 @@ object EhCookieStore : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
for (cookie in cookies) {
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
if (cookie.name != "__utmp") {
addCookie(cookie)
}
addCookie(cookie)
}
}
@@ -20,6 +20,7 @@ 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
@@ -49,6 +50,7 @@ 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
@@ -320,6 +322,10 @@ 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) {
@@ -560,7 +566,7 @@ object EhEngine {
gidArray: LongArray,
tokenArray: Array<String?>, dstCat: Int
): Void? {
check(gidArray.size == tokenArray.size)
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
var i = 0
val n = gidArray.size
while (i < n) {
@@ -841,30 +847,6 @@ 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.allFilter
val list = EhDB.getAllFilter()
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,9 +20,10 @@ 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.copyToFile
import com.hippo.yorozuya.IOUtils
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -36,6 +37,7 @@ 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
@@ -195,21 +197,22 @@ 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)
runCatching {
try {
call.execute().use { response ->
if (!response.isSuccessful) {
return false
}
response.body.use {
it.copyToFile(file)
}
val body = response.body
body.byteStream()
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
return true
}
}.onFailure {
file.delete()
it.printStackTrace()
} catch (t: Throwable) {
FileUtils.delete(file)
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
return false
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
@@ -46,7 +46,6 @@ 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,12 +15,31 @@
* 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
import org.jsoup.Jsoup
package com.hippo.ehviewer.client.parser;
object EventPaneParser {
fun parse(body: String): String? {
return Jsoup.parse(body).getElementById("eventpane")?.html()
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;
}
}
@@ -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.AssertUtilsKt;
import com.hippo.yorozuya.AssertUtils;
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"
AssertUtilsKt.assertEquals(11, fps.size());
AssertUtils.assertEquals(11, fps.size());
for (int i = 0; i < 10; i++) {
Element fp = fps.get(i);
@@ -36,7 +36,6 @@ 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
@@ -338,7 +337,7 @@ object GalleryDetailParser {
// Id
val a = element.previousElementSibling()
val name = a!!.attr("name")
comment.id = name trimAnd { substring(1).toInt().toLong() }
comment.id = StringUtils.trim(name).substring(1).toInt().toLong()
// Editable, vote up and vote down
val c4 = JsoupUtils.getElementByClass(element, "c4")
if (null != c4) {
@@ -1,21 +0,0 @@
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
}
@@ -1,78 +0,0 @@
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(
{},
{}, // Only original request's onStart and onCancel will get called
{ 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) }
}
fun onCancel(key: String) {
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)
}
}
}
}
@@ -1,105 +0,0 @@
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,9 +37,10 @@ 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>
@@ -67,7 +68,7 @@ object DownloadManager : OnSpiderListener {
init {
// Get all labels
val labels = EhDB.allDownloadLabelList.toMutableList()
val labels = EhDB.getAllDownloadLabelList()
mLabelList = labels
// Create list for each label
@@ -81,7 +82,7 @@ object DownloadManager : OnSpiderListener {
mDefaultInfoList = LinkedList()
// Get all info
val allInfoList = EhDB.allDownloadInfo
val allInfoList = EhDB.getAllDownloadInfo()
mAllInfoList = LinkedList(allInfoList)
// Create all info map
@@ -93,21 +94,14 @@ object DownloadManager : OnSpiderListener {
allInfoMap.put(info.gid, info)
// Add to each label list
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)) {
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)) {
// Add label to DB and list
labels.add(EhDB.addDownloadLabel(label))
labels.add(EhDB.addDownloadLabel(info.label))
}
}
list.add(info)
@@ -118,9 +112,6 @@ object DownloadManager : OnSpiderListener {
}
private fun getInfoListForLabel(label: String?): LinkedList<DownloadInfo>? {
contract {
returns(null) implies (label != null)
}
return if (label == null) {
mDefaultInfoList
} else {
@@ -353,7 +344,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)
@@ -847,7 +838,7 @@ object DownloadManager : OnSpiderListener {
override fun onPageFailure(
index: Int,
error: String?,
error: String,
finished: Int,
downloaded: Int,
total: Int
@@ -869,11 +860,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
}
@@ -1089,7 +1080,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,6 +30,7 @@ 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
@@ -65,7 +66,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = DownloadManager
mDownloadManager = EhApplication.downloadManager
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
@@ -0,0 +1,352 @@
/*
* 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);
}
}
}
}
}
@@ -1,168 +0,0 @@
/*
* 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>) {}
}
@@ -0,0 +1,217 @@
/*
* 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;
}
}
}
}
@@ -1,155 +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 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
}
}
}
@@ -0,0 +1,61 @@
/*
* 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);
}
@@ -1,48 +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 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"
)
@@ -0,0 +1,118 @@
/*
* 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);
}
}
}
@@ -1,74 +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 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,111 +15,145 @@
*/
package com.hippo.ehviewer.preference
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import com.hippo.ehviewer.download.DownloadManager as downloadManager
import androidx.preference.Preference
import com.hippo.ehviewer.EhApplication.Companion.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 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
import com.hippo.util.ExceptionUtils
import kotlinx.coroutines.runBlocking
import java.io.IOException
import java.util.Collections
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
class RestoreDownloadPreference constructor(
context: Context, attrs: AttributeSet?
) : TaskPreference(context, attrs) {
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
}
RestoreItem().apply {
gid = spiderInfo.gid
token = spiderInfo.token
dirname = file.name
}
}.getOrNull()
override fun onCreateTask(): Task {
return RestoreTask(context)
}
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 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) {
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
}
}
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 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
}
@@ -0,0 +1,208 @@
/*
* 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();
}
}
}
}
@@ -1,53 +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.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,7 +26,13 @@ class VersionPreference @JvmOverloads constructor(
) : Preference(context, attrs) {
init {
setTitle(R.string.settings_about_version)
summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" +
context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
@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
}
}
@@ -16,13 +16,10 @@
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
@@ -31,9 +28,7 @@ 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.coil.edit
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
import com.hippo.ehviewer.gallery.PageLoader2
import com.hippo.image.Image.CloseableSource
import com.hippo.unifile.RawFile
import com.hippo.unifile.UniFile
@@ -106,27 +101,30 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
* @param extension with dot
*/
private fun fixExtension(extension: String): String {
return extension.takeIf { SUPPORT_IMAGE_EXTENSIONS.contains(it) }
?: SUPPORT_IMAGE_EXTENSIONS[0]
return extension.takeIf { PageLoader2.SUPPORT_IMAGE_EXTENSIONS.contains(it) }
?: PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]
}
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
val dir = downloadDir ?: return false
val key = EhCacheKeyFactory.getImageKey(mGid, index)
return runCatching {
sCache.read(key) {
val extension = fixExtension("." + metadata.toFile().readText())
runCatching {
(sCache[key] ?: return false).use { snapshot ->
val extension = fixExtension("." + snapshot.metadata.toFile().readText())
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
(file.openOutputStream() as FileOutputStream).use { outputStream ->
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
it copyTo outputStream.fd
outputStream.channel.use { outChannel ->
snapshot.data.toFile().inputStream().use { inputStream ->
inputStream.channel.use {
outChannel.transferFrom(it, 0, it.size())
}
}
}
}
}
}.getOrElse {
it.printStackTrace()
false
return true
}
return false
}
operator fun contains(index: Int): Boolean {
@@ -209,17 +207,19 @@ 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
runSuspendCatching {
sCache.edit(key) {
metadata.toFile().writeText(extension)
data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
}
editor.metadata.toFile().writeText(extension)
editor.data.toFile().outputStream().use { outputStream ->
outputStream.channel.use { received = doSave(it) }
}
}.onFailure {
editor.abort()
it.printStackTrace()
return false
}.onSuccess {
editor.commit()
return received == length
}
}
@@ -334,7 +334,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
).build()
}
private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
private val COMPAT_IMAGE_EXTENSIONS = PageLoader2.SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
fun getGalleryDownloadDir(gid: Long): UniFile? {
val dir = Settings.getDownloadLocation()
@@ -1,14 +1,6 @@
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(
@@ -26,50 +18,3 @@ 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"
@@ -0,0 +1,57 @@
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"
@@ -0,0 +1,748 @@
/*
* 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);
}
public 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);
}
}
@@ -1,830 +0,0 @@
/*
* 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
@@ -0,0 +1,341 @@
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.ehviewer.spider.SpiderQueen.STATE_DOWNLOADING
import com.hippo.image.Image.Companion.decode
import com.hippo.util.ExceptionUtils
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
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
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 = 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)
}
}.apply {
invokeOnCompletion {
if (queen.getPageState(index) == STATE_DOWNLOADING) {
Log.d(DEBUG_TAG, "Download image cancelled $index")
launchNonCancellable {
spiderDen.remove(index)
}
}
}
}
}
}
@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, 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 (check509(it.imageUrl)) {
// 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 (check509(it.imageUrl)) {
// 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
forceHtml = true
}
Log.d(DEBUG_TAG, "End download image $index")
}
spiderDen.remove(index)
queen.updatePageState(index, STATE_FAILED, error)
}
companion object {
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 fun check509(url: String) = url in URL_509_ARRAY
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.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
@@ -16,6 +16,7 @@
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
@@ -40,11 +41,10 @@ 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,9 +79,10 @@ class AdvancedFragment : BasePreferenceFragment() {
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show()
lifecycleScope.launchIO {
IoThreadPoolExecutor.getInstance().execute {
val success = EhDB.exportDB(requireActivity(), uri)
withUIContext {
val activity: Activity? = activity
activity?.runOnUiThread {
if (alertDialog.isShowing) {
alertDialog.dismiss()
}
@@ -203,9 +204,10 @@ class AdvancedFragment : BasePreferenceFragment() {
.setCancelable(false)
.setView(R.layout.preference_dialog_task)
.show()
lifecycleScope.launchIO {
IoThreadPoolExecutor.getInstance().execute {
val error = EhDB.importDB(requireActivity(), uri)
withUIContext {
val activity: Activity? = activity
activity?.runOnUiThread {
if (alertDialog.isShowing) {
alertDialog.dismiss()
}
@@ -23,17 +23,12 @@ 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() {
@@ -62,7 +57,6 @@ 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!!)
@@ -72,27 +66,6 @@ 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
}
}
}
}
@@ -132,10 +105,6 @@ 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,10 +78,6 @@ 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.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
@@ -70,6 +70,7 @@ 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
@@ -151,6 +152,8 @@ 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) {
@@ -246,7 +249,10 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
container!!.addView(mFabLayout)
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
mViewTransition = ViewTransition(content, mTip)
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_download)
val context = context
AssertUtils.assertNotNull(content)
val resources = context!!.resources
val drawable = ContextCompat.getDrawable(context, R.drawable.big_download)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mAdapter = DownloadAdapter()
@@ -416,6 +422,8 @@ 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)
@@ -423,7 +431,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
val id = item.itemId
if (id == R.id.action_add) {
val builder =
EditTextDialogBuilder(requireContext(), null, getString(R.string.download_labels))
EditTextDialogBuilder(context!!, null, getString(R.string.download_labels))
builder.setTitle(R.string.new_label_title)
builder.setPositiveButton(android.R.string.ok, null)
val dialog = builder.show()
@@ -441,7 +449,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
items[i + 2] = list[i].label
i++
}
BaseDialogBuilder(requireContext())
BaseDialogBuilder(context!!)
.setTitle(R.string.default_download_label)
.setItems(items) { _: DialogInterface?, which: Int ->
if (which == 0) {
@@ -986,6 +994,7 @@ 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,6 +63,7 @@ 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
@@ -131,6 +132,8 @@ 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()
@@ -183,6 +186,7 @@ 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)
}
@@ -192,6 +196,7 @@ 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(
@@ -199,6 +204,9 @@ 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))
@@ -324,7 +332,9 @@ 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 ->
@@ -335,7 +345,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(requireContext())
BaseDialogBuilder(context!!)
.setTitle(R.string.default_favorites_collection)
.setItems(items) { _: DialogInterface?, which: Int ->
Settings.putDefaultFavSlot(
@@ -646,8 +656,8 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
private fun onGetFavoritesLocal(keyword: String?, taskId: Int) {
if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {
val list: List<GalleryInfo> = if (keyword.isNullOrEmpty()) {
EhDB.allLocalFavorites
val list: List<GalleryInfo> = if (TextUtils.isEmpty(keyword)) {
EhDB.getAllLocalFavorites()
} 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.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.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.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
import com.hippo.ehviewer.EhDB
@@ -89,7 +89,6 @@ 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
@@ -107,6 +106,7 @@ 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,9 +264,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
mGalleryInfo?.let {
viewLifecycleOwner.lifecycleScope.launchIO {
runCatching {
val queen = SpiderQueen.obtainSpiderQueen(it, MODE_READ)
val startPage = queen.awaitStartPage()
SpiderQueen.releaseSpiderQueen(queen, MODE_READ)
val startPage = SpiderQueen.getStartPage(it.gid)
withUIContext {
binding.content.header.read.text = if (startPage == 0) {
getString(R.string.read)
@@ -363,6 +361,8 @@ 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,6 +426,8 @@ 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
@@ -434,6 +436,8 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
}
private fun prepareData(): Boolean {
val context = context
AssertUtils.assertNotNull(context)
if (mGalleryDetail != null) {
return true
}
@@ -447,7 +451,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
if (mGalleryDetail != null) {
return true
}
val application = requireContext().applicationContext as EhApplication
val application = context!!.applicationContext as EhApplication
return if (application.containGlobalStuff(mRequestId)) {
// request exist
true
@@ -570,7 +574,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
_binding ?: return
val resources = resources
binding.content.header.run {
(thumb as LoadImageView).load(gd.thumb!!, false)
(thumb as LoadImageView).load(gd.thumb!!, true)
setTitle(EhUtils.getSuitableTitle(gd))
uploader.text = gd.uploader
if (gd.disowned) {
@@ -648,8 +652,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
val tag = inflater.inflate(R.layout.item_gallery_tag, awl, false) as TextView
awl.addView(tag)
val tagStr = if (it.startsWith('_')) {
val alpha = ColorUtils.setAlphaComponent(tag.currentTextColor, 128)
tag.setTextColor(alpha)
tag.alpha = 0.5f
it.substring(1)
} else {
it
@@ -726,14 +729,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)
}
}
}
@@ -1194,7 +1197,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
if (null == context || -1L == gid) {
return
}
val downloadState = com.hippo.ehviewer.download.DownloadManager.getDownloadState(gid)
val downloadState = 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.download.DownloadManager as downloadManager
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.FavouriteStatusRouter
@@ -545,7 +545,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
drawerBinding.recyclerViewDrawer.visibility = View.GONE
}
lifecycleScope.launchIO {
mQuickSearchList = EhDB.allQuickSearch.toMutableList()
mQuickSearchList = EhDB.getAllQuickSearch()
if (mQuickSearchList.isNotEmpty()) {
withUIContext {
drawerBinding.tip.visibility = View.GONE
@@ -43,7 +43,6 @@ 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
@@ -74,7 +73,7 @@ class HistoryScene : BaseToolbarScene() {
}
})
}
private val mDownloadManager = DownloadManager
private val mDownloadManager = EhApplication.downloadManager
private val mDownloadInfoListener: DownloadInfoListener by lazy {
object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
@@ -132,7 +131,7 @@ class HistoryScene : BaseToolbarScene() {
val historyData = Pager(
PagingConfig(20)
) {
EhDB.historyLazyList
EhDB.getHistoryLazyList()
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
binding.recyclerView.adapter = mAdapter
val layoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
@@ -297,7 +296,7 @@ class HistoryScene : BaseToolbarScene() {
}
3 -> {
val labelRawList = DownloadManager.labelList
val labelRawList = EhApplication.downloadManager.labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
labelList.add(getString(R.string.default_download_label_name))
var i = 0
@@ -363,7 +362,7 @@ class HistoryScene : BaseToolbarScene() {
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val downloadManager = DownloadManager
val downloadManager = EhApplication.downloadManager
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
downloadManager.changeLabel(listOf(downloadInfo), label)
@@ -28,6 +28,7 @@ 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
/**
@@ -129,7 +130,9 @@ 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 drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_sad_pandroid)
val context = context
AssertUtils.assertNotNull(context)
val drawable = ContextCompat.getDrawable(context!!, R.drawable.big_sad_pandroid)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip!!.setCompoundDrawables(null, drawable, null, null)
mTip!!.setOnClickListener(this)
@@ -61,7 +61,7 @@ fun HistoryScreen(hostFragment: HistoryComposeScreenFragmentBridge) {
Pager(
PagingConfig(20)
) {
EhDB.historyLazyList
EhDB.getHistoryLazyList()
}.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 = DownloadManager
private val downloadManager = EhApplication.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 = DownloadManager
private val downloadManager = EhApplication.downloadManager
@Composable
private fun DialogSelectorItem(
@@ -10,16 +10,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import com.hippo.ehviewer.R
import kotlin.math.ceil
private val colorYellow800 = Color(0xfff9a825)
@Composable
fun SimpleRatingWidget(rating: Float) {
val r = ceil(rating * 2).toInt().coerceIn(0, 10)
val fullStar = r / 2
val r = (rating * 2).toInt().coerceAtLeast(0).coerceAtMost(10)
val fullStar = r.floorDiv(2)
val halfStar = r % 2
val outlineStar = 5 - fullStar - halfStar
val colorYellow800 = Color(0xfff9a825)
Row {
repeat(fullStar) {
Icon(
@@ -73,4 +73,9 @@ public class FixedThumb extends LoadImageView {
setScaleType(ScaleType.FIT_CENTER);
}
@Override
public void onPreSetImageResource(int resId, boolean isTarget) {
setScaleType(ScaleType.FIT_CENTER);
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.widget;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import androidx.core.content.ContextCompat;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
/**
* 5 stars, from 0 to 10
*/
public class SimpleRatingView extends View {
private Drawable mStarDrawable;
private Drawable mStarHalfDrawable;
private Drawable mStarOutlineDrawable;
private int mRatingSize;
private int mRatingInterval;
private float mRating;
private int mRatingInt;
public SimpleRatingView(Context context) {
super(context);
init(context);
}
public SimpleRatingView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SimpleRatingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
Resources resources = context.getResources();
mStarDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_x16);
mStarHalfDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_half_x16);
mStarOutlineDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_outline_x16);
mRatingSize = resources.getDimensionPixelOffset(R.dimen.rating_size);
mRatingInterval = resources.getDimensionPixelOffset(R.dimen.rating_interval);
mStarDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
mStarHalfDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
mStarOutlineDrawable.setBounds(0, 0, mRatingSize, mRatingSize);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mRatingSize * 5 + mRatingInterval * 4, mRatingSize);
}
@Override
protected void onDraw(Canvas canvas) {
int ratingInt = mRatingInt;
int step = mRatingSize + mRatingInterval;
int numStar = ratingInt / 2;
int numStarHalf = ratingInt % 2;
int saved = canvas.save();
while (numStar-- > 0) {
mStarDrawable.draw(canvas);
canvas.translate(step, 0);
}
if (numStarHalf == 1) {
mStarHalfDrawable.draw(canvas);
canvas.translate(step, 0);
}
int numOutline = 5 - numStar - numStarHalf;
while (numOutline-- > 0) {
mStarOutlineDrawable.draw(canvas);
canvas.translate(step, 0);
}
canvas.restoreToCount(saved);
}
public float getRating() {
return mRating;
}
public void setRating(float rating) {
if (mRating != rating) {
mRating = rating;
int ratingInt = MathUtils.clamp((int) Math.ceil(rating * 2), 0, 10);
if (mRatingInt != ratingInt) {
mRatingInt = ratingInt;
invalidate();
}
}
}
}
@@ -1,26 +0,0 @@
package com.hippo.ehviewer.widget
import android.content.Context
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.hippo.ehviewer.ui.widget.SimpleRatingWidget
class SimpleRatingView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : AbstractComposeView(context, attrs, defStyle) {
var rating by mutableStateOf(0f)
@Composable
override fun Content() {
Mdc3Theme {
SimpleRatingWidget(rating)
}
}
}
@@ -0,0 +1,92 @@
/*
* 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);
}
}
}
}
@@ -0,0 +1,78 @@
/*
* 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,50 +17,99 @@
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.appcompat.content.res.AppCompatResources
import coil.load
import androidx.core.content.ContextCompat
import coil.imageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import coil.size.Size
import com.hippo.drawable.PreciselyClipDrawable
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.coil.MergeInterceptor
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 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() }
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 fun setRetry(canRetry: Boolean) {
when (mRetryType) {
RETRY_TYPE_CLICK -> {
setOnClickListener(if (canRetry) this else null)
isClickable = canRetry
}
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0)
setRetryType(a.getInt(R.styleable.LoadImageView_retryType, 0))
a.recycle()
}
RETRY_TYPE_LONG_CLICK -> {
setOnLongClickListener(if (canRetry) this else null)
isLongClickable = canRetry
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (mFailed) onFailure()
else task?.run { if (isDisposed) reload() }
}
RETRY_TYPE_NONE -> {}
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)
}
}
}
}
@@ -78,48 +127,76 @@ open class LoadImageView @JvmOverloads constructor(
mClipHeight = Integer.MIN_VALUE
}
fun load(url: String, crossfade: Boolean = true) {
mUrl = url
load(url) {
size(Size.ORIGINAL)
if (!crossfade) crossfade(false)
listener(
{ setRetry(false) },
{
MergeInterceptor.onCancel(url)
setRetry(true)
},
{ _, _ ->
super.setImageDrawable(errorDrawable)
setRetry(true)
},
{ _, _ -> setRetry(false) }
)
}
@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)
}
private fun reload() {
mUrl?.let { this.load(it) }
mRequest?.let { load(it) }
}
override fun setImageDrawable(drawable: Drawable?) {
var newDrawable = drawable
if (newDrawable != null) {
if (Integer.MIN_VALUE != mOffsetX) {
newDrawable =
PreciselyClipDrawable(newDrawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
private fun onWait() {
mFailed = false
clearDrawable()
}
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)
}
onPreSetImageDrawable(newDrawable, true)
}
super.setImageDrawable(newDrawable)
}
override fun getDrawable(): Drawable? {
var newDrawable = super.getDrawable()
if (newDrawable is PreciselyClipDrawable) {
newDrawable = newDrawable.drawable
RETRY_TYPE_LONG_CLICK -> {
setOnLongClickListener(this)
}
else -> {
mRequest = null
}
}
return newDrawable
}
override fun onClick(v: View) {
@@ -133,6 +210,8 @@ 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
@@ -0,0 +1,24 @@
/*
* 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);
}
}
@@ -0,0 +1,24 @@
/*
* 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);
}
}
@@ -0,0 +1,122 @@
/*
* 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);
}
}
}
@@ -1,5 +0,0 @@
package com.hippo.yorozuya
fun assertEquals(expected: Int, actual: Int) {
check(expected == actual)
}
@@ -1,19 +0,0 @@
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,8 +18,6 @@ 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;
@@ -32,6 +30,25 @@ 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() {
}
@@ -40,7 +57,7 @@ public final class StringUtils {
* Unescape xml. It do not work perfectly.
*/
public static String unescapeXml(String str) {
return Parser.unescapeEntities(str, true);
return replaceEach(str, ESCAPE_CHARATER_LIST, UNESCAPE_CHARATER_LIST);
}
/**
@@ -124,6 +141,218 @@ 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])) {
@@ -235,6 +464,38 @@ 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,11 +1,5 @@
package com.hippo.yorozuya
import org.jsoup.parser.Parser
fun String.unescapeXml(): String {
return Parser.unescapeEntities(this, true)
}
inline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T {
return block(trim())
return StringUtils.unescapeXml(this)
}
@@ -0,0 +1,50 @@
/*
* 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();
}
}
}
@@ -0,0 +1,40 @@
/*
* 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();
}
}
@@ -0,0 +1,42 @@
/*
* 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,6 +33,8 @@ 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
@@ -78,6 +80,7 @@ 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
@@ -85,7 +88,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
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.withUIContext
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.toggle
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
@@ -94,22 +98,18 @@ 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.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.max
@@ -161,11 +161,12 @@ class ReaderActivity : EhActivity() {
}
}
var mGalleryProvider: PageLoader2? = null
private var mSize: Int = 0
private var mCurrentIndex: Int = 0
private var mSavingPage = -1
private lateinit var builder: EditTextDialogBuilder
private var builder: EditTextDialogBuilder? = null
private var dialogShown = false
private lateinit var dialog: AlertDialog
private var dialog: AlertDialog? = null
private var requestStoragePermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { result ->
@@ -190,13 +191,16 @@ class ReaderActivity : EhActivity() {
return EhUrl.getGalleryDetailUrl(gid, token, 0, false)
}
private fun buildProvider(replace: Boolean = false) {
private fun buildProvider() {
if (mGalleryProvider != null) {
if (replace) mGalleryProvider!!.stop() else return
return
}
if (ACTION_EH == mAction) {
mGalleryInfo?.let { mGalleryProvider = EhPageLoader(it) }
if (mGalleryInfo != null) {
mGalleryProvider =
EhPageLoader(mGalleryInfo)
}
} else if (Intent.ACTION_VIEW == mAction) {
if (mUri != null) {
try {
@@ -209,56 +213,19 @@ class ReaderActivity : EhActivity() {
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()
}
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))
}
}
}
)
mGalleryProvider =
ArchivePageLoader(this, mUri)
}
}
}
private fun handleIntent(intent: Intent?) {
intent ?: return
private fun onInit() {
val intent = intent ?: return
mAction = intent.action
mFilename = intent.getStringExtra(KEY_FILENAME)
mUri = intent.data
mGalleryInfo = intent.getParcelableExtraCompat(KEY_GALLERY_INFO)
mPage = intent.getIntExtra(KEY_PAGE, -1)
}
private fun onInit() {
handleIntent(intent)
buildProvider()
}
@@ -272,21 +239,6 @@ class ReaderActivity : EhActivity() {
buildProvider()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent(intent)
buildProvider(true)
mGalleryProvider?.let {
lifecycleScope.launchIO {
it.start()
if (it.awaitReady()) withUIContext {
viewer?.setGalleryProvider(it)
moveToPageIndex(0)
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_ACTION, mAction)
@@ -312,19 +264,29 @@ 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.let {
if (it == null) {
finish()
return
}
lifecycleScope.launchIO {
it.start()
if (it.awaitReady()) withUIContext { setGallery() }
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()
}
}
}
@@ -333,13 +295,7 @@ class ReaderActivity : EhActivity() {
}
fun setGallery() {
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)
@@ -579,6 +535,56 @@ 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"
@@ -729,13 +735,12 @@ class ReaderActivity : EhActivity() {
readerPreferences.defaultReadingMode().set(newReadingMode.flagValue)
setGallery()
// updateCropBordersShortcut()
updateCropBordersShortcut()
}
}
}
// Crop borders
/*
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
@@ -756,7 +761,6 @@ class ReaderActivity : EhActivity() {
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
*/
// Rotation
with(binding.actionRotation) {
@@ -797,7 +801,6 @@ class ReaderActivity : EhActivity() {
binding.actionRotation.setImageResource(orientation.iconRes)
}
/*
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(readerPreferences.defaultReadingMode().get())
val enabled = if (isPagerType) {
@@ -814,7 +817,6 @@ class ReaderActivity : EhActivity() {
},
)
}
*/
/**
@@ -61,7 +61,7 @@ class ReaderPageSheet(
}
private fun refresh() {
activity.mGalleryProvider?.retryPage(page.index)
activity.mGalleryProvider?.retryPage(page)
dismiss()
}
}
@@ -7,19 +7,21 @@ 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)
abstract suspend fun awaitReady(): Boolean
abstract val isReady: Boolean
var state = MutableStateFlow(STATE_WAIT)
abstract val error: String
abstract fun start()
@@ -32,31 +34,25 @@ abstract class PageLoader {
mImageCache.evictAll()
}
abstract val size: Int
abstract fun size(): Int
fun request(index: Int) {
fun request(page: ReaderPage) {
val index = mPages.indexOf(page)
val image = mImageCache[index]
if (image != null) {
if (image != null)
notifyPageSucceed(index, image)
} else {
notifyPageWait(index)
else
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(index: Int) {
notifyPageWait(index)
onForceRequest(index)
fun retryPage(page: ReaderPage) {
page.status.value = Page.State.QUEUE
onForceRequest(mPages.indexOf(page))
}
protected abstract fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>)
@@ -65,12 +61,19 @@ abstract class PageLoader {
protected abstract fun onForceRequest(index: Int)
fun cancelRequest(index: Int) {
onCancelRequest(index)
fun cancelRequest(page: ReaderPage) {
onCancelRequest(mPages.indexOf(page))
}
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
}
@@ -92,13 +95,7 @@ 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)
}
@@ -116,4 +113,10 @@ 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,6 +18,10 @@ 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
@@ -26,6 +30,8 @@ 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.
@@ -131,9 +137,21 @@ 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
}
@@ -234,7 +252,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
private fun setNonAnimatedImage(
image: Drawable,
image: Any,
config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
@@ -255,8 +273,14 @@ open class ReaderPageImageView @JvmOverloads constructor(
},
)
val bitmap = (image as BitmapDrawable).bitmap
setImage(ImageSource.cachedBitmap(bitmap))
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}")
}
isVisible = true
}
@@ -300,17 +324,36 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
private fun setAnimatedImage(
image: Drawable,
image: Any,
config: Config,
) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) {
setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
}
setImageDrawable(image)
(image as? Animatable)?.start()
isVisible = true
this@ReaderPageImageView.onImageLoaded()
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)
}
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 })
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff. Show more