Compare commits
149
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3d07f9e4e | ||
|
|
e5500b978c | ||
|
|
2fea94ce9b | ||
|
|
90b6106fb4 | ||
|
|
6e15d1e480 | ||
|
|
d35588b993 | ||
|
|
957ecf0fe1 | ||
|
|
83dc47c3b5 | ||
|
|
fa4d35487c | ||
|
|
624a20c9bf | ||
|
|
58cd5c2896 | ||
|
|
bb8a802d65 | ||
|
|
f757e98da0 | ||
|
|
49661c0bf0 | ||
|
|
42f36b8e74 | ||
|
|
9bc3c9a902 | ||
|
|
1dd04740bb | ||
|
|
35201e6615 | ||
|
|
86d416bccf | ||
|
|
979841665a | ||
|
|
c29b8c114b | ||
|
|
a03eae3c87 | ||
|
|
704c2a261e | ||
|
|
8fabdd1926 | ||
|
|
f9df373a16 | ||
|
|
f959f98012 | ||
|
|
e12a93f36b | ||
|
|
c3aca0a88d | ||
|
|
add7819945 | ||
|
|
3c6d3bbd80 | ||
|
|
66e1315b08 | ||
|
|
9bbe010fc9 | ||
|
|
4f81d164c8 | ||
|
|
c808e9135d | ||
|
|
1e7861ef15 | ||
|
|
a70b8c6dff | ||
|
|
ce86638e8e | ||
|
|
06ab978f53 | ||
|
|
9c2026982c | ||
|
|
863b910311 | ||
|
|
68da3b7580 | ||
|
|
43556f219b | ||
|
|
6022144dce | ||
|
|
18a0e3fd9e | ||
|
|
6c7c6e4b22 | ||
|
|
ffa7a74575 | ||
|
|
37f9c28133 | ||
|
|
ea7a012e0a | ||
|
|
0d594da6c9 | ||
|
|
ed588bf26c | ||
|
|
b6673fd828 | ||
|
|
060bcc76ef | ||
|
|
8fc8dd2960 | ||
|
|
1dc36275aa | ||
|
|
f305e68202 | ||
|
|
5c481df41f | ||
|
|
e86c626536 | ||
|
|
44559bab1c | ||
|
|
2ae28f1c63 | ||
|
|
b3e16d2b2a | ||
|
|
6f186080df | ||
|
|
c9d9335b71 | ||
|
|
22c8a36224 | ||
|
|
37f705d517 | ||
|
|
8b9985a7dc | ||
|
|
f51e585d8d | ||
|
|
a6c204e444 | ||
|
|
a0a215400e | ||
|
|
37e3166f98 | ||
|
|
ddccdc2ade | ||
|
|
755a15d5e5 | ||
|
|
5c6716bd98 | ||
|
|
7d87417095 | ||
|
|
77bb6e9a82 | ||
|
|
019174d7e2 | ||
|
|
cac88cbb81 | ||
|
|
25500da486 | ||
|
|
53f2c7126b | ||
|
|
eb95026a47 | ||
|
|
6b0f8bc325 | ||
|
|
a80f41a89a | ||
|
|
8690f8aa8d | ||
|
|
d9c55a284b | ||
|
|
c8a10a8b75 | ||
|
|
8e8dc76c3d | ||
|
|
725679f5d4 | ||
|
|
dfce07d7eb | ||
|
|
73c9e37eba | ||
|
|
ccd5bd8394 | ||
|
|
fd9b7db999 | ||
|
|
f3fe115733 | ||
|
|
16f3f74a90 | ||
|
|
4a6b85003a | ||
|
|
bf651c7e40 | ||
|
|
473ff4d308 | ||
|
|
5d64e8c61b | ||
|
|
b559bb3904 | ||
|
|
359f250b3b | ||
|
|
a20336a11a | ||
|
|
0313c5705f | ||
|
|
14469638b3 | ||
|
|
d15c5fc9d1 | ||
|
|
16e7d266fe | ||
|
|
b716a98548 | ||
|
|
39f40ed906 | ||
|
|
a644a1cee1 | ||
|
|
61fd8dc778 | ||
|
|
145d5e6302 | ||
|
|
ad071e48df | ||
|
|
28b5717f5b | ||
|
|
41b818caa6 | ||
|
|
cf87a654fb | ||
|
|
9ffab097bb | ||
|
|
bdc3c2b6b5 | ||
|
|
3f39662ba1 | ||
|
|
aa485547a6 | ||
|
|
747f23bfe1 | ||
|
|
d885841826 | ||
|
|
0c457ef61d | ||
|
|
9462f1455b | ||
|
|
c07325e71e | ||
|
|
5eda7b3fc3 | ||
|
|
6b0c208b75 | ||
|
|
b0c6a26bea | ||
|
|
67a0893a97 | ||
|
|
d7438a26fb | ||
|
|
ee7ab38028 | ||
|
|
8d999ea278 | ||
|
|
0851cf50a0 | ||
|
|
0b6676aaa3 | ||
|
|
c64167c76a | ||
|
|
72a6419aff | ||
|
|
64e26a6b8b | ||
|
|
23184df1b9 | ||
|
|
dd123d7b87 | ||
|
|
0e5d1e2325 | ||
|
|
4939ce37c3 | ||
|
|
6e7e668d39 | ||
|
|
2d086a5c6e | ||
|
|
f7e592eac0 | ||
|
|
c13abafb09 | ||
|
|
eeed0720f7 | ||
|
|
f099b74aa6 | ||
|
|
f7f54692a7 | ||
|
|
b97eff4284 | ||
|
|
f234dc61bc | ||
|
|
83b40d2ccb | ||
|
|
a8d3d71ea9 | ||
|
|
621772f359 |
No files matched your search
@@ -1,9 +1,3 @@
|
||||
[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
|
||||
+31
-13
@@ -1,3 +1,8 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
@@ -8,7 +13,7 @@ plugins {
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "33.0.1"
|
||||
buildToolsVersion = "33.0.2"
|
||||
ndkVersion = "25.2.9519653"
|
||||
|
||||
splits {
|
||||
@@ -30,12 +35,26 @@ android {
|
||||
enableV4Signing = true
|
||||
}
|
||||
|
||||
val commitSha by lazy {
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString().trim()
|
||||
}
|
||||
|
||||
val buildTime by lazy {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
|
||||
formatter.format(Instant.now())
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 180031
|
||||
versionName = "1.8.6.0-alpha04"
|
||||
versionCode = 180035
|
||||
versionName = "1.8.7.0"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -52,6 +71,8 @@ android {
|
||||
"nb-rNO"
|
||||
)
|
||||
)
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -72,15 +93,12 @@ android {
|
||||
"-progressive",
|
||||
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
@@ -125,7 +143,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.2"
|
||||
kotlinCompilerExtensionVersion = "1.4.3"
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
@@ -144,7 +162,7 @@ dependencies {
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.10.0-alpha02")
|
||||
implementation("androidx.core:core-ktx:1.10.0-beta01")
|
||||
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha07")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
@@ -169,6 +187,7 @@ dependencies {
|
||||
implementation("androidx.room:room-paging:$room_version")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.0")
|
||||
implementation("com.drakeet.drawer:drawer:1.0.3")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0") // Dead Dependency
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") // Dead Dependency
|
||||
@@ -189,12 +208,11 @@ dependencies {
|
||||
implementation("dev.rikka.rikkax.preference:simplemenu-preference:1.0.3")
|
||||
implementation("dev.rikka.rikkax.material:material-preference:2.0.0")
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
// implementation 'io.coil-kt:coil:2.2.2'
|
||||
implementation("com.github.Ehviewer-Overhauled.coil:coil-compose:53302f2")
|
||||
implementation("io.coil-kt:coil-compose:2.2.2")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0-RC")
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.5.0")
|
||||
implementation("org.jsoup:jsoup:1.15.4")
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<activity
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
project(ehviewer)
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(ehviewer C)
|
||||
include(FetchContent)
|
||||
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -ffunction-sections -fdata-sections -funroll-loops -flto=thin \
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto=thin \
|
||||
-mllvm -polly \
|
||||
-mllvm -polly-run-dce \
|
||||
-mllvm -polly-run-inliner \
|
||||
@@ -12,6 +14,20 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inli
|
||||
-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)
|
||||
@@ -27,11 +43,7 @@ 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)
|
||||
@@ -39,12 +51,17 @@ SET(HAVE_LZMADEC_H 1)
|
||||
SET(HAVE_LIBLZMADEC 1)
|
||||
|
||||
# Configure libarchive link's static lib
|
||||
SET(LIBARCHIVE_CUSTOM_LIBS "liblzma" "nettle")
|
||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||
|
||||
# Build libarchive
|
||||
add_subdirectory(libarchive)
|
||||
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 and link our app's native lib
|
||||
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)
|
||||
add_library(${PROJECT_NAME} SHARED archive.c fdutils.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log)
|
||||
+95
-33
@@ -17,14 +17,12 @@
|
||||
* EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/mman.h>
|
||||
#include <syscall.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
@@ -34,6 +32,7 @@
|
||||
|
||||
#define LOG_TAG "libarchive_wrapper"
|
||||
|
||||
#include "natsort/strnatcmp.h"
|
||||
#include "ehviewer.h"
|
||||
|
||||
typedef struct {
|
||||
@@ -46,12 +45,25 @@ typedef struct {
|
||||
typedef struct {
|
||||
const char *filename;
|
||||
int index;
|
||||
size_t size;
|
||||
} entry;
|
||||
|
||||
pthread_mutex_t ctx_lock;
|
||||
static archive_ctx **ctx_pool;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
static void *mempool = NULL;
|
||||
static size_t *mempoolofs = NULL;
|
||||
|
||||
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
|
||||
|
||||
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
||||
|
||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||
|
||||
static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
@@ -59,8 +71,6 @@ static size_t archiveSize = 0;
|
||||
static entry *entries = NULL;
|
||||
static size_t entryCount = 0;
|
||||
|
||||
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
|
||||
|
||||
const char supportExt[9][6] = {
|
||||
"jpeg",
|
||||
"jpg",
|
||||
@@ -87,9 +97,7 @@ static inline int filename_is_playable_file(const char *name) {
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
int a_len = (int) strlen(fa);
|
||||
int b_len = (int) strlen(fb);
|
||||
return a_len == b_len ? strcmp(fa, fb) : a_len - b_len;
|
||||
return strnatcmp(fa, fb);
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
@@ -99,6 +107,7 @@ static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
if (filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
entries[count].size = archive_entry_size(ctx->entry);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -108,6 +117,48 @@ static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
return count;
|
||||
}
|
||||
|
||||
static bool archive_prealloc_mempool() {
|
||||
mempoolofs = calloc(entryCount, sizeof(size_t));
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
if (!i)
|
||||
mempoolofs[i] = PAGE_ALIGN(entries[i].size);
|
||||
else
|
||||
mempoolofs[i] = PAGE_ALIGN(entries[i].size) + mempoolofs[i - 1];
|
||||
}
|
||||
mempool = mmap(0, MEMPOOL_SIZE, PROT_RW, MAP_ANON_POOL, -1, 0);
|
||||
if (mempool == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Mincore does not support anon mapping yet
|
||||
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
|
||||
static bool is_mempool_pages_present_and_lock(int index) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = MEMPOOL_ENTITY_SIZE(index);
|
||||
mlock(addr, size);
|
||||
#if 0
|
||||
unsigned char vec[size / PAGE_SIZE];
|
||||
unsigned char r = 1;
|
||||
mincore(addr, size, vec);
|
||||
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
|
||||
return r;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
|
||||
|
||||
static void mempool_release_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
munlock(addr, size);
|
||||
int ret = madvise(addr, size, MADV_FREE);
|
||||
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
@@ -158,7 +209,6 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
}
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
assert(!(ctx->next_index > index));
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
continue;
|
||||
@@ -233,8 +283,6 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
EH_UNUSED(thiz);
|
||||
archive_ctx *ctx = NULL;
|
||||
int mmap_flags = MAP_PRIVATE;
|
||||
if (size <= POPULATE_THRESHOLD)
|
||||
mmap_flags |= MAP_POPULATE;
|
||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||
@@ -255,7 +303,7 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
@@ -292,41 +340,52 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
if (!archive_prealloc_mempool()) {
|
||||
r = 0;
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
}
|
||||
|
||||
archive_release_ctx(ctx);
|
||||
return r;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||
#pragma ide diagnostic ignored "UnreachableCode"
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_extractToByteBuffer(JNIEnv *env, jobject thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return 0;
|
||||
long long size = archive_entry_size(ctx->entry);
|
||||
void *buffer = malloc(size);
|
||||
if (!buffer) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = entries[index].size;
|
||||
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
ret = archive_read_data(ctx->arc, addr, size);
|
||||
ctx->using = 0;
|
||||
LOGE("Allocate buffer for decompression failed:ENOMEM");
|
||||
return 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
mempool_release_pages(addr, size);
|
||||
} else {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
}
|
||||
ret = archive_read_data(ctx->arc, buffer, size);
|
||||
ctx->using = 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, buffer, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
free(buffer);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
EH_UNUSED(env);
|
||||
@@ -339,6 +398,8 @@ Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
|
||||
passwd = NULL;
|
||||
need_encrypt = false;
|
||||
munmap(archiveAddr, archiveSize);
|
||||
munmap(mempool, MEMPOOL_SIZE);
|
||||
free(mempoolofs);
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
@@ -415,5 +476,6 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hippo_UriArchiveAccessor_releaseByteBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
free(addr);
|
||||
}
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
mempool_release_pages(addr, size);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <jni.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sendfile.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
@@ -25,3 +27,10 @@ Java_com_hippo_Native_getFd(JNIEnv *env, jclass clazz, jobject fileDescriptor) {
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_Native_sendfile(JNIEnv *env, jclass clazz, jint from, jint to) {
|
||||
struct stat st;
|
||||
fstat(from, &st);
|
||||
sendfile(to, from, 0, st.st_size);
|
||||
}
|
||||
Submodule app/src/main/cpp/libarchive deleted from c9298fd6dc.
Submodule app/src/main/cpp/liblzma deleted from fc9fb0ea24.
@@ -0,0 +1,177 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
|
||||
/* partial change history:
|
||||
*
|
||||
* 2004-10-10 mbp: Lift out character type dependencies into macros.
|
||||
*
|
||||
* Eric Sosman pointed out that ctype functions take a parameter whose
|
||||
* value must be that of an unsigned int, even on platforms that have
|
||||
* negative chars in their default char type.
|
||||
*/
|
||||
|
||||
#include <stddef.h> /* size_t */
|
||||
#include <ctype.h>
|
||||
|
||||
#include "strnatcmp.h"
|
||||
|
||||
|
||||
/* These are defined as macros to make it easier to adapt this code to
|
||||
* different characters types or comparison functions. */
|
||||
static inline int
|
||||
nat_isdigit(nat_char a)
|
||||
{
|
||||
return isdigit((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline int
|
||||
nat_isspace(nat_char a)
|
||||
{
|
||||
return isspace((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline nat_char
|
||||
nat_toupper(nat_char a)
|
||||
{
|
||||
return toupper((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_right(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
int bias = 0;
|
||||
|
||||
/* The longest run of digits wins. That aside, the greatest
|
||||
value wins, but we can't know that it will until we've scanned
|
||||
both numbers to know that they have the same magnitude, so we
|
||||
remember it in BIAS. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return bias;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b) {
|
||||
if (!bias)
|
||||
bias = -1;
|
||||
} else if (*a > *b) {
|
||||
if (!bias)
|
||||
bias = +1;
|
||||
} else if (!*a && !*b)
|
||||
return bias;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_left(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
/* Compare two left-aligned numbers: the first to have a
|
||||
different value wins. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return 0;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b)
|
||||
return -1;
|
||||
if (*a > *b)
|
||||
return +1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||
{
|
||||
int ai, bi;
|
||||
nat_char ca, cb;
|
||||
int fractional, result;
|
||||
|
||||
ai = bi = 0;
|
||||
while (1) {
|
||||
ca = a[ai]; cb = b[bi];
|
||||
|
||||
/* skip over leading spaces or zeros */
|
||||
while (nat_isspace(ca))
|
||||
ca = a[++ai];
|
||||
|
||||
while (nat_isspace(cb))
|
||||
cb = b[++bi];
|
||||
|
||||
/* process run of digits */
|
||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||
fractional = (ca == '0' || cb == '0');
|
||||
|
||||
if (fractional) {
|
||||
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
} else {
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ca && !cb) {
|
||||
/* The strings compare the same. Perhaps the caller
|
||||
will want to call strcmp to break the tie. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fold_case) {
|
||||
ca = nat_toupper(ca);
|
||||
cb = nat_toupper(cb);
|
||||
}
|
||||
|
||||
if (ca < cb)
|
||||
return -1;
|
||||
|
||||
if (ca > cb)
|
||||
return +1;
|
||||
|
||||
++ai; ++bi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
strnatcmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Compare, recognizing numeric string and ignoring case. */
|
||||
int
|
||||
strnatcasecmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 1);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* CUSTOMIZATION SECTION
|
||||
*
|
||||
* You can change this typedef, but must then also change the inline
|
||||
* functions in strnatcmp.c */
|
||||
typedef char nat_char;
|
||||
|
||||
int strnatcmp(nat_char const *a, nat_char const *b);
|
||||
int strnatcasecmp(nat_char const *a, nat_char const *b);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -9,7 +9,7 @@ import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavOptions
|
||||
|
||||
fun onNavDestinationSelected2(item: MenuItem, navController: NavController): Boolean {
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return false
|
||||
if (navController.currentDestination?.matchDestination(item.itemId) == true) return true
|
||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
||||
if (
|
||||
navController.currentDestination!!.parent!!.findNode(item.itemId)
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package com.davemorrissey.labs.subscaleview
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
|
||||
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Helper class used to set the source and additional attributes from a variety of sources. Supports
|
||||
@@ -83,38 +76,6 @@ class ImageSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
|
||||
*
|
||||
* @param resId resource ID.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun resource(context: Context, resId: Int): ImageSource {
|
||||
return ImageSource(ResourceInputProvider(context, resId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an asset name.
|
||||
*
|
||||
* @param assetName asset name.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun asset(context: Context, assetName: String): ImageSource {
|
||||
return ImageSource(AssetInputProvider(context, assetName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from a URI.
|
||||
*
|
||||
* @param uri image URI.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun uri(context: Context, uri: Uri): ImageSource {
|
||||
return ImageSource(UriInputProvider(context, uri))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input provider.
|
||||
*
|
||||
@@ -125,16 +86,6 @@ class ImageSource {
|
||||
return ImageSource(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from an input stream.
|
||||
*
|
||||
* @param stream open input stream.
|
||||
* @return an [ImageSource] instance.
|
||||
*/
|
||||
fun inputStream(stream: InputStream): ImageSource {
|
||||
return ImageSource(OpenStreamProvider(stream))
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a loaded bitmap for display.
|
||||
*
|
||||
|
||||
@@ -19,12 +19,9 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.IntDef
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.asset
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource.Companion.resource
|
||||
import com.davemorrissey.labs.subscaleview.decoder.Decoder
|
||||
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.hippo.ehviewer.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -303,42 +300,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
setMinimumDpi(160)
|
||||
setDoubleTapZoomDpi(160)
|
||||
setGestureDetector(context)
|
||||
|
||||
// Handle XML attributes
|
||||
if (attr != null) {
|
||||
val typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.SubsamplingScaleImageView)
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_assetName)) {
|
||||
val assetName = typedAttr.getString(R.styleable.SubsamplingScaleImageView_assetName)
|
||||
if (assetName != null && assetName.isNotEmpty()) {
|
||||
setImage(asset(context, assetName))
|
||||
}
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_src)) {
|
||||
val resId = typedAttr.getResourceId(R.styleable.SubsamplingScaleImageView_src, 0)
|
||||
if (resId > 0) {
|
||||
setImage(resource(context, resId))
|
||||
}
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_panEnabled)) {
|
||||
setPanEnabled(typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_panEnabled, true))
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_zoomEnabled)) {
|
||||
isZoomEnabled = typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_zoomEnabled, true)
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_quickScaleEnabled)) {
|
||||
isQuickScaleEnabled =
|
||||
typedAttr.getBoolean(R.styleable.SubsamplingScaleImageView_quickScaleEnabled, true)
|
||||
}
|
||||
if (typedAttr.hasValue(R.styleable.SubsamplingScaleImageView_tileBackgroundColor)) {
|
||||
setTileBackgroundColor(
|
||||
typedAttr.getColor(
|
||||
R.styleable.SubsamplingScaleImageView_tileBackgroundColor,
|
||||
Color.argb(0, 0, 0, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
typedAttr.recycle()
|
||||
}
|
||||
quickScaleThreshold =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
||||
}
|
||||
@@ -1953,20 +1914,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
val scaleHeight = scale * sHeight
|
||||
when (panLimit) {
|
||||
PAN_LIMIT_CENTER -> {
|
||||
vTarget.top = -(vTranslate!!.y - height / 2).coerceAtLeast(0F)
|
||||
vTarget.left = -(vTranslate!!.x - width / 2).coerceAtLeast(0F)
|
||||
vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
|
||||
vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
|
||||
vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
|
||||
vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
|
||||
}
|
||||
PAN_LIMIT_OUTSIDE -> {
|
||||
vTarget.top = -(vTranslate!!.y - height).coerceAtLeast(0F)
|
||||
vTarget.left = -(vTranslate!!.x - width).coerceAtLeast(0F)
|
||||
vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
|
||||
vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
|
||||
vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
|
||||
vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
|
||||
}
|
||||
else -> {
|
||||
vTarget.top = -vTranslate!!.y.coerceAtLeast(0F)
|
||||
vTarget.left = -vTranslate!!.x.coerceAtLeast(0F)
|
||||
vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
|
||||
vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
|
||||
vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
|
||||
vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import kotlin.Throws
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import java.lang.Exception
|
||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
||||
|
||||
class ImageDecoder @JvmOverloads constructor(
|
||||
private val cropBorders: Boolean,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
|
||||
private val assets = context.assets
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = assets.open(assetName)
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
interface InputProvider {
|
||||
@Throws(IOException::class)
|
||||
fun openStream(): InputStream?
|
||||
}
|
||||
interface InputProvider
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.Throws
|
||||
|
||||
/**
|
||||
* Provider from an already open input stream.
|
||||
*
|
||||
*
|
||||
* Please keep in mind this provider is not reusable.
|
||||
*/
|
||||
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = stream
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import java.io.IOException
|
||||
|
||||
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
|
||||
private val resources = context.resources
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resources.openRawResource(resource)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.davemorrissey.labs.subscaleview.provider
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
import kotlin.Throws
|
||||
|
||||
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
|
||||
private val resolver = context.contentResolver
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun openStream() = resolver.openInputStream(uri)
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package com.hippo
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.FileDescriptor
|
||||
|
||||
object Native {
|
||||
@@ -26,4 +27,22 @@ object Native {
|
||||
|
||||
@JvmStatic
|
||||
external fun getFd(fd: FileDescriptor?): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun sendfile(from: Int, to: Int)
|
||||
}
|
||||
|
||||
val FileDescriptor.fd
|
||||
get() = Native.getFd(this)
|
||||
|
||||
infix fun ParcelFileDescriptor.receivefrom(fd: FileDescriptor) {
|
||||
Native.sendfile(fd.fd, getFd())
|
||||
}
|
||||
|
||||
infix fun ParcelFileDescriptor.copyTo(fd: FileDescriptor) {
|
||||
Native.sendfile(getFd(), fd.fd)
|
||||
}
|
||||
|
||||
infix fun ParcelFileDescriptor.copyTo(fd: ParcelFileDescriptor) {
|
||||
Native.sendfile(getFd(), fd.fd)
|
||||
}
|
||||
@@ -20,12 +20,14 @@ package com.hippo
|
||||
import android.content.Context
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.hippo.image.Image
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
class UriArchiveAccessor(ctx: Context, private val uri: Uri) {
|
||||
val pfd by lazy { ctx.contentResolver.openFileDescriptor(uri, "r")!! }
|
||||
fun open(): Int {
|
||||
Log.d(DEBUG_TAG, "Open archive $uri")
|
||||
return openArchive(pfd.fd, pfd.statSize)
|
||||
}
|
||||
|
||||
@@ -39,6 +41,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
fun close() {
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
Log.d(DEBUG_TAG, "Close archive $uri successfully!")
|
||||
}
|
||||
|
||||
fun getImageSource(index: Int): Image.CloseableSource? {
|
||||
@@ -61,4 +64,6 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) {
|
||||
@JvmStatic
|
||||
private external fun releaseByteBuffer(buffer: ByteBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEBUG_TAG = "UriArchiveAccessor"
|
||||
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
public class DrawableWrapper extends Drawable implements Drawable.Callback {
|
||||
private Drawable mDrawable;
|
||||
private Rect mBounds;
|
||||
|
||||
public DrawableWrapper(Drawable drawable) {
|
||||
this.setWrappedDrawable(drawable);
|
||||
}
|
||||
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (mDrawable instanceof BitmapDrawable bitmapDrawable) {
|
||||
canvas.drawBitmap(bitmapDrawable.getBitmap(), null, mBounds, bitmapDrawable.getPaint());
|
||||
} else {
|
||||
synchronized (mDrawable) {
|
||||
mDrawable.setBounds(mBounds);
|
||||
mDrawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
mBounds = bounds;
|
||||
}
|
||||
|
||||
public int getChangingConfigurations() {
|
||||
return this.mDrawable.getChangingConfigurations();
|
||||
}
|
||||
|
||||
public void setChangingConfigurations(int configs) {
|
||||
this.mDrawable.setChangingConfigurations(configs);
|
||||
}
|
||||
|
||||
public void setDither(boolean dither) {
|
||||
this.mDrawable.setDither(dither);
|
||||
}
|
||||
|
||||
public void setFilterBitmap(boolean filter) {
|
||||
this.mDrawable.setFilterBitmap(filter);
|
||||
}
|
||||
|
||||
public void setAlpha(int alpha) {
|
||||
this.mDrawable.setAlpha(alpha);
|
||||
}
|
||||
|
||||
public void setColorFilter(ColorFilter cf) {
|
||||
this.mDrawable.setColorFilter(cf);
|
||||
}
|
||||
|
||||
public boolean isStateful() {
|
||||
return this.mDrawable.isStateful();
|
||||
}
|
||||
|
||||
public boolean setState(@NonNull int[] stateSet) {
|
||||
return this.mDrawable.setState(stateSet);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public int[] getState() {
|
||||
return this.mDrawable.getState();
|
||||
}
|
||||
|
||||
public void jumpToCurrentState() {
|
||||
this.mDrawable.jumpToCurrentState();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Drawable getCurrent() {
|
||||
return this.mDrawable.getCurrent();
|
||||
}
|
||||
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
return super.setVisible(visible, restart) || this.mDrawable.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
public int getOpacity() {
|
||||
return this.mDrawable.getOpacity();
|
||||
}
|
||||
|
||||
public Region getTransparentRegion() {
|
||||
return this.mDrawable.getTransparentRegion();
|
||||
}
|
||||
|
||||
public int getIntrinsicWidth() {
|
||||
return this.mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
public int getIntrinsicHeight() {
|
||||
return this.mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
public int getMinimumWidth() {
|
||||
return this.mDrawable.getMinimumWidth();
|
||||
}
|
||||
|
||||
public int getMinimumHeight() {
|
||||
return this.mDrawable.getMinimumHeight();
|
||||
}
|
||||
|
||||
public boolean getPadding(@NonNull Rect padding) {
|
||||
return this.mDrawable.getPadding(padding);
|
||||
}
|
||||
|
||||
public void invalidateDrawable(@NonNull Drawable who) {
|
||||
this.invalidateSelf();
|
||||
}
|
||||
|
||||
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||
this.scheduleSelf(what, when);
|
||||
}
|
||||
|
||||
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||
this.unscheduleSelf(what);
|
||||
}
|
||||
|
||||
protected boolean onLevelChange(int level) {
|
||||
return this.mDrawable.setLevel(level);
|
||||
}
|
||||
|
||||
public boolean isAutoMirrored() {
|
||||
return DrawableCompat.isAutoMirrored(this.mDrawable);
|
||||
}
|
||||
|
||||
public void setAutoMirrored(boolean mirrored) {
|
||||
DrawableCompat.setAutoMirrored(this.mDrawable, mirrored);
|
||||
}
|
||||
|
||||
public void setTint(int tint) {
|
||||
DrawableCompat.setTint(this.mDrawable, tint);
|
||||
}
|
||||
|
||||
public void setTintList(ColorStateList tint) {
|
||||
DrawableCompat.setTintList(this.mDrawable, tint);
|
||||
}
|
||||
|
||||
public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
|
||||
DrawableCompat.setTintMode(this.mDrawable, tintMode);
|
||||
}
|
||||
|
||||
public void setHotspot(float x, float y) {
|
||||
DrawableCompat.setHotspot(this.mDrawable, x, y);
|
||||
}
|
||||
|
||||
public void setHotspotBounds(int left, int top, int right, int bottom) {
|
||||
DrawableCompat.setHotspotBounds(this.mDrawable, left, top, right, bottom);
|
||||
}
|
||||
|
||||
public Drawable getWrappedDrawable() {
|
||||
return this.mDrawable;
|
||||
}
|
||||
|
||||
public void setWrappedDrawable(Drawable drawable) {
|
||||
if (this.mDrawable != null) {
|
||||
this.mDrawable.setCallback(null);
|
||||
}
|
||||
|
||||
this.mDrawable = drawable;
|
||||
if (drawable != null) {
|
||||
drawable.setCallback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.drawable;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
/**
|
||||
* Show a part of the original drawable
|
||||
*/
|
||||
public class PreciselyClipDrawable extends DrawableWrapper {
|
||||
|
||||
private final boolean mClip;
|
||||
private RectF mScale;
|
||||
private Rect mTemp;
|
||||
|
||||
public PreciselyClipDrawable(Drawable drawable, int offsetX, int offsetY, int width, int height) {
|
||||
super(drawable);
|
||||
float originWidth = drawable.getIntrinsicWidth();
|
||||
float originHeight = drawable.getIntrinsicHeight();
|
||||
|
||||
if (originWidth <= 0 || originHeight <= 0) {
|
||||
// Can not clip
|
||||
mClip = false;
|
||||
} else {
|
||||
mClip = true;
|
||||
mScale = new RectF();
|
||||
mScale.set(MathUtils.clamp(offsetX / originWidth, 0.0f, 1.0f),
|
||||
MathUtils.clamp(offsetY / originHeight, 0.0f, 1.0f),
|
||||
MathUtils.clamp((offsetX + width) / originWidth, 0.0f, 1.0f),
|
||||
MathUtils.clamp((offsetY + height) / originHeight, 0.0f, 1.0f));
|
||||
mTemp = new Rect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
if (mClip) {
|
||||
if (!mScale.isEmpty()) {
|
||||
mTemp.left = (int) ((mScale.left * bounds.right - mScale.right * bounds.left) /
|
||||
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
|
||||
mTemp.right = (int) (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) /
|
||||
(mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left)));
|
||||
mTemp.top = (int) ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) /
|
||||
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
|
||||
mTemp.bottom = (int) (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) /
|
||||
(mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top)));
|
||||
super.onBoundsChange(mTemp);
|
||||
}
|
||||
} else {
|
||||
super.onBoundsChange(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
if (mClip) {
|
||||
return (int) (super.getIntrinsicWidth() * mScale.width());
|
||||
} else {
|
||||
return super.getIntrinsicWidth();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
if (mClip) {
|
||||
return (int) (super.getIntrinsicHeight() * mScale.height());
|
||||
} else {
|
||||
return super.getIntrinsicHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (mClip) {
|
||||
if (!mScale.isEmpty()) {
|
||||
int saveCount = canvas.save();
|
||||
canvas.clipRect(getBounds());
|
||||
super.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
} else {
|
||||
super.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.drawable
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.DrawableWrapper
|
||||
import androidx.core.graphics.withClip
|
||||
|
||||
/**
|
||||
* Show a part of the original drawable
|
||||
*/
|
||||
class PreciselyClipDrawable(
|
||||
drawable: Drawable,
|
||||
offsetX: Int,
|
||||
offsetY: Int,
|
||||
width: Int,
|
||||
height: Int
|
||||
) : DrawableWrapper(drawable) {
|
||||
private val mScale: RectF
|
||||
private val mTemp = Rect()
|
||||
|
||||
init {
|
||||
val originWidth = drawable.intrinsicWidth.toFloat()
|
||||
val originHeight = drawable.intrinsicHeight.toFloat()
|
||||
mScale = RectF(
|
||||
(offsetX / originWidth).coerceIn(0.0f, 1.0f),
|
||||
(offsetY / originHeight).coerceIn(0.0f, 1.0f),
|
||||
((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),
|
||||
((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||
mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()
|
||||
mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||
mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()
|
||||
super.onBoundsChange(mTemp)
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return (super.getIntrinsicWidth() * mScale.width()).toInt()
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return (super.getIntrinsicHeight() * mScale.height()).toInt()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.withClip(bounds) { super.draw(canvas) }
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Debug;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.util.PackageUtils;
|
||||
import com.hippo.util.ReadableTime;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class Crash {
|
||||
private Crash() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String avoidNull(String str) {
|
||||
return null != str ? str : "null";
|
||||
}
|
||||
|
||||
private static void collectInfo(Context context, FileWriter fw) throws IOException {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
|
||||
if (pi != null) {
|
||||
String versionName = pi.versionName == null ? "null" : pi.versionName;
|
||||
String versionCode = String.valueOf(pi.versionCode);
|
||||
fw.write("======== PackageInfo ========\r\n");
|
||||
fw.write("PackageName=");
|
||||
fw.write(pi.packageName);
|
||||
fw.write("\r\n");
|
||||
fw.write("VersionName=");
|
||||
fw.write(versionName);
|
||||
fw.write("\r\n");
|
||||
fw.write("VersionCode=");
|
||||
fw.write(versionCode);
|
||||
fw.write("\r\n");
|
||||
String signature = PackageUtils.getSignature(context, pi.packageName);
|
||||
fw.write("Signature=");
|
||||
fw.write(null != signature ? signature : "null");
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
fw.write("======== PackageInfo ========\r\n");
|
||||
fw.write("Can't get package information\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
|
||||
// Runtime
|
||||
String topActivityClazzName = "null";
|
||||
fw.write("======== Runtime ========\r\n");
|
||||
fw.write("TopActivity=");
|
||||
fw.write(avoidNull(topActivityClazzName));
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
|
||||
// Device info
|
||||
fw.write("======== DeviceInfo ========\r\n");
|
||||
fw.write("BOARD=");
|
||||
fw.write(Build.BOARD);
|
||||
fw.write("\r\n");
|
||||
fw.write("BOOTLOADER=");
|
||||
fw.write(Build.BOOTLOADER);
|
||||
fw.write("\r\n");
|
||||
fw.write("SUPPORTED_ABIS=");
|
||||
fw.write(Arrays.toString(Build.SUPPORTED_ABIS));
|
||||
fw.write("\r\n");
|
||||
fw.write("DEVICE=");
|
||||
fw.write(Build.DEVICE);
|
||||
fw.write("\r\n");
|
||||
fw.write("DISPLAY=");
|
||||
fw.write(Build.DISPLAY);
|
||||
fw.write("\r\n");
|
||||
fw.write("FINGERPRINT=");
|
||||
fw.write(Build.FINGERPRINT);
|
||||
fw.write("\r\n");
|
||||
fw.write("HARDWARE=");
|
||||
fw.write(Build.HARDWARE);
|
||||
fw.write("\r\n");
|
||||
fw.write("HOST=");
|
||||
fw.write(Build.HOST);
|
||||
fw.write("\r\n");
|
||||
fw.write("ID=");
|
||||
fw.write(Build.ID);
|
||||
fw.write("\r\n");
|
||||
fw.write("MANUFACTURER=");
|
||||
fw.write(Build.MANUFACTURER);
|
||||
fw.write("\r\n");
|
||||
fw.write("MODEL=");
|
||||
fw.write(Build.MODEL);
|
||||
fw.write("\r\n");
|
||||
fw.write("PRODUCT=");
|
||||
fw.write(Build.PRODUCT);
|
||||
fw.write("\r\n");
|
||||
fw.write("RADIO=");
|
||||
fw.write(Build.getRadioVersion());
|
||||
fw.write("\r\n");
|
||||
fw.write("TAGS=");
|
||||
fw.write(Build.TAGS);
|
||||
fw.write("\r\n");
|
||||
fw.write("TYPE=");
|
||||
fw.write(Build.TYPE);
|
||||
fw.write("\r\n");
|
||||
fw.write("USER=");
|
||||
fw.write(Build.USER);
|
||||
fw.write("\r\n");
|
||||
fw.write("CODENAME=");
|
||||
fw.write(Build.VERSION.CODENAME);
|
||||
fw.write("\r\n");
|
||||
fw.write("INCREMENTAL=");
|
||||
fw.write(Build.VERSION.INCREMENTAL);
|
||||
fw.write("\r\n");
|
||||
fw.write("RELEASE=");
|
||||
fw.write(Build.VERSION.RELEASE);
|
||||
fw.write("\r\n");
|
||||
fw.write("SDK=");
|
||||
fw.write(Integer.toString(Build.VERSION.SDK_INT));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY=");
|
||||
fw.write(
|
||||
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_NATIVE=");
|
||||
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_MAX=");
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("MEMORY_TOTAL=");
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false));
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
}
|
||||
|
||||
private static void getThrowableInfo(Throwable t, FileWriter fw) {
|
||||
PrintWriter printWriter = new PrintWriter(fw);
|
||||
t.printStackTrace(printWriter);
|
||||
Throwable cause = t.getCause();
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(printWriter);
|
||||
cause = cause.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveCrashLog(Context context, Throwable t) {
|
||||
File dir = AppConfig.getExternalCrashDir();
|
||||
if (dir == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis());
|
||||
String fileName = "crash-" + nowString + ".log";
|
||||
File file = new File(dir, fileName);
|
||||
|
||||
FileWriter fw = null;
|
||||
try {
|
||||
fw = new FileWriter(file);
|
||||
fw.write("TIME=");
|
||||
fw.write(nowString);
|
||||
fw.write("\r\n");
|
||||
fw.write("\r\n");
|
||||
collectInfo(context, fw);
|
||||
fw.write("======== CrashInfo ========\r\n");
|
||||
getThrowableInfo(t, fw);
|
||||
fw.write("\r\n");
|
||||
fw.flush();
|
||||
} catch (Exception e) {
|
||||
file.delete();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Debug
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.OSUtils
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.util.Arrays
|
||||
|
||||
object Crash {
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun collectInfo(fw: FileWriter) {
|
||||
fw.write("======== PackageInfo ========\r\n")
|
||||
fw.write("PackageName=")
|
||||
fw.write(BuildConfig.APPLICATION_ID)
|
||||
fw.write("\r\n")
|
||||
fw.write("VersionName=")
|
||||
fw.write(BuildConfig.VERSION_NAME)
|
||||
fw.write("\r\n")
|
||||
fw.write("VersionCode=")
|
||||
fw.write(BuildConfig.VERSION_CODE)
|
||||
fw.write("\r\n")
|
||||
fw.write("CommitSha=")
|
||||
fw.write(BuildConfig.COMMIT_SHA)
|
||||
fw.write("\r\n")
|
||||
fw.write("BuildTime=")
|
||||
fw.write(BuildConfig.BUILD_TIME)
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
|
||||
// Runtime
|
||||
val topActivityClazzName = EhApplication.application.topActivity?.javaClass?.name
|
||||
fw.write("======== Runtime ========\r\n")
|
||||
fw.write("TopActivity=")
|
||||
fw.write(topActivityClazzName ?: "null")
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
|
||||
// Device info
|
||||
fw.write("======== DeviceInfo ========\r\n")
|
||||
fw.write("BOARD=")
|
||||
fw.write(Build.BOARD)
|
||||
fw.write("\r\n")
|
||||
fw.write("BOOTLOADER=")
|
||||
fw.write(Build.BOOTLOADER)
|
||||
fw.write("\r\n")
|
||||
fw.write("SUPPORTED_ABIS=")
|
||||
fw.write(Arrays.toString(Build.SUPPORTED_ABIS))
|
||||
fw.write("\r\n")
|
||||
fw.write("DEVICE=")
|
||||
fw.write(Build.DEVICE)
|
||||
fw.write("\r\n")
|
||||
fw.write("DISPLAY=")
|
||||
fw.write(Build.DISPLAY)
|
||||
fw.write("\r\n")
|
||||
fw.write("FINGERPRINT=")
|
||||
fw.write(Build.FINGERPRINT)
|
||||
fw.write("\r\n")
|
||||
fw.write("HARDWARE=")
|
||||
fw.write(Build.HARDWARE)
|
||||
fw.write("\r\n")
|
||||
fw.write("HOST=")
|
||||
fw.write(Build.HOST)
|
||||
fw.write("\r\n")
|
||||
fw.write("ID=")
|
||||
fw.write(Build.ID)
|
||||
fw.write("\r\n")
|
||||
fw.write("MANUFACTURER=")
|
||||
fw.write(Build.MANUFACTURER)
|
||||
fw.write("\r\n")
|
||||
fw.write("MODEL=")
|
||||
fw.write(Build.MODEL)
|
||||
fw.write("\r\n")
|
||||
fw.write("PRODUCT=")
|
||||
fw.write(Build.PRODUCT)
|
||||
fw.write("\r\n")
|
||||
fw.write("RADIO=")
|
||||
fw.write(Build.getRadioVersion())
|
||||
fw.write("\r\n")
|
||||
fw.write("TAGS=")
|
||||
fw.write(Build.TAGS)
|
||||
fw.write("\r\n")
|
||||
fw.write("TYPE=")
|
||||
fw.write(Build.TYPE)
|
||||
fw.write("\r\n")
|
||||
fw.write("USER=")
|
||||
fw.write(Build.USER)
|
||||
fw.write("\r\n")
|
||||
fw.write("CODENAME=")
|
||||
fw.write(Build.VERSION.CODENAME)
|
||||
fw.write("\r\n")
|
||||
fw.write("INCREMENTAL=")
|
||||
fw.write(Build.VERSION.INCREMENTAL)
|
||||
fw.write("\r\n")
|
||||
fw.write("RELEASE=")
|
||||
fw.write(Build.VERSION.RELEASE)
|
||||
fw.write("\r\n")
|
||||
fw.write("SDK=")
|
||||
fw.write(Build.VERSION.SDK_INT.toString())
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY=")
|
||||
fw.write(
|
||||
FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false)
|
||||
)
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_NATIVE=")
|
||||
fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_MAX=")
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("MEMORY_TOTAL=")
|
||||
fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
}
|
||||
|
||||
private fun getThrowableInfo(t: Throwable, fw: FileWriter) {
|
||||
val printWriter = PrintWriter(fw)
|
||||
t.printStackTrace(printWriter)
|
||||
var cause = t.cause
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(printWriter)
|
||||
cause = cause.cause
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCrashLog(t: Throwable) {
|
||||
val dir = AppConfig.getExternalCrashDir() ?: return
|
||||
val nowString = ReadableTime.getFilenamableTime(System.currentTimeMillis())
|
||||
val fileName = "crash-$nowString.log"
|
||||
val file = File(dir, fileName)
|
||||
var fw: FileWriter? = null
|
||||
try {
|
||||
fw = FileWriter(file)
|
||||
fw.write("TIME=")
|
||||
fw.write(nowString)
|
||||
fw.write("\r\n")
|
||||
fw.write("\r\n")
|
||||
collectInfo(fw)
|
||||
fw.write("======== CrashInfo ========\r\n")
|
||||
getThrowableInfo(t, fw)
|
||||
fw.write("\r\n")
|
||||
fw.flush()
|
||||
} catch (e: Exception) {
|
||||
file.delete()
|
||||
} finally {
|
||||
IOUtils.closeQuietly(fw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,31 +18,23 @@ 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.client.parser.EventPaneParser
|
||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||
import com.hippo.ehviewer.dao.buildMainDB
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
@@ -55,8 +47,6 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.Proxy
|
||||
import java.security.KeyStore
|
||||
@@ -87,7 +77,7 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
try {
|
||||
if (Settings.getSaveCrashLog()) {
|
||||
Crash.saveCrashLog(application, e)
|
||||
Crash.saveCrashLog(e)
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
@@ -106,14 +96,11 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
ehDatabase
|
||||
}
|
||||
launchIO {
|
||||
downloadManager
|
||||
DownloadManager.isIdle
|
||||
}
|
||||
launchIO {
|
||||
cleanupDownload()
|
||||
}
|
||||
launchIO {
|
||||
theDawnOfNewDay()
|
||||
}
|
||||
}
|
||||
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
|
||||
}
|
||||
@@ -140,54 +127,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
}
|
||||
}
|
||||
|
||||
private fun theDawnOfNewDay() {
|
||||
if (!Settings.getRequestNews()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ehCookieStore.hasSignedIn()) {
|
||||
val referer = EhUrl.REFERER_E
|
||||
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
try {
|
||||
call.execute().use { response ->
|
||||
val responseBody = response.body
|
||||
val body = responseBody.string()
|
||||
val html = EventPaneParser.parse(body)
|
||||
if (html != null) {
|
||||
showEventPane(html)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showEventPane(html: String) {
|
||||
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
|
||||
return
|
||||
}
|
||||
val activity = topActivity
|
||||
activity?.runOnUiThread {
|
||||
val dialog = BaseDialogBuilder(activity)
|
||||
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
dialog.setOnShowListener {
|
||||
val messageView = dialog.findViewById<View>(android.R.id.message)
|
||||
if (messageView is TextView) {
|
||||
messageView.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
try {
|
||||
dialog.show()
|
||||
} catch (t: Throwable) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearTempDir() {
|
||||
var dir = AppConfig.getTempDir()
|
||||
if (null != dir) {
|
||||
@@ -243,16 +182,12 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
locked = true
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this).apply {
|
||||
okHttpClient(okHttpClient)
|
||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||
// Coil spawns a new thread for every image load by default
|
||||
fetcherDispatcher(Dispatchers.IO.limitedParallelism(8))
|
||||
decoderDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
transformationDispatcher(Dispatchers.IO.limitedParallelism(2))
|
||||
crossfade(true)
|
||||
components { add(MergeInterceptor) }
|
||||
crossfade(300)
|
||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -300,9 +235,6 @@ class EhApplication : Application(), DefaultLifecycleObserver, ImageLoaderFactor
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val downloadManager by lazy { DownloadManager() }
|
||||
|
||||
@JvmStatic
|
||||
val hosts by lazy { Hosts(application, "hosts.db") }
|
||||
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.PagingSource;
|
||||
import androidx.room.Room;
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.dao.BasicDao;
|
||||
import com.hippo.ehviewer.dao.DownloadDirname;
|
||||
import com.hippo.ehviewer.dao.DownloadDirnameDao;
|
||||
import com.hippo.ehviewer.dao.DownloadInfo;
|
||||
import com.hippo.ehviewer.dao.DownloadLabel;
|
||||
import com.hippo.ehviewer.dao.DownloadLabelDao;
|
||||
import com.hippo.ehviewer.dao.DownloadsDao;
|
||||
import com.hippo.ehviewer.dao.EhDatabase;
|
||||
import com.hippo.ehviewer.dao.Filter;
|
||||
import com.hippo.ehviewer.dao.HistoryDao;
|
||||
import com.hippo.ehviewer.dao.HistoryInfo;
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
|
||||
import com.hippo.ehviewer.dao.LocalFavoritesDao;
|
||||
import com.hippo.ehviewer.dao.QuickSearch;
|
||||
import com.hippo.ehviewer.dao.QuickSearchDao;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.ObjectUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EhDB {
|
||||
private static final int CUR_DB_VER = 4;
|
||||
|
||||
private static final EhDatabase db = EhApplication.getEhDatabase();
|
||||
|
||||
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
|
||||
switch (oldVersion) {
|
||||
case 1: // 1 to 2, add FILTER
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS \"FILTER\" (" + //
|
||||
"\"_id\" INTEGER PRIMARY KEY ," + // 0: id
|
||||
"\"MODE\" INTEGER NOT NULL ," + // 1: mode
|
||||
"\"TEXT\" TEXT," + // 2: text
|
||||
"\"ENABLE\" INTEGER);"); // 3: enable
|
||||
case 2: // 2 to 3, add ENABLE column to table FILTER
|
||||
db.execSQL("CREATE TABLE " + "\"FILTER2\" (" +
|
||||
"\"_id\" INTEGER PRIMARY KEY ," +
|
||||
"\"MODE\" INTEGER NOT NULL ," +
|
||||
"\"TEXT\" TEXT," +
|
||||
"\"ENABLE\" INTEGER);");
|
||||
db.execSQL("INSERT INTO \"FILTER2\" (" +
|
||||
"_id, MODE, TEXT, ENABLE)" +
|
||||
"SELECT _id, MODE, TEXT, 1 FROM FILTER;");
|
||||
db.execSQL("DROP TABLE FILTER");
|
||||
db.execSQL("ALTER TABLE FILTER2 RENAME TO FILTER");
|
||||
case 3: // 3 to 4, add PAGE_FROM and PAGE_TO column to QUICK_SEARCH
|
||||
db.execSQL("CREATE TABLE " + "\"QUICK_SEARCH2\" (" +
|
||||
"\"_id\" INTEGER PRIMARY KEY ," +
|
||||
"\"NAME\" TEXT," +
|
||||
"\"MODE\" INTEGER NOT NULL ," +
|
||||
"\"CATEGORY\" INTEGER NOT NULL ," +
|
||||
"\"KEYWORD\" TEXT," +
|
||||
"\"ADVANCE_SEARCH\" INTEGER NOT NULL ," +
|
||||
"\"MIN_RATING\" INTEGER NOT NULL ," +
|
||||
"\"PAGE_FROM\" INTEGER NOT NULL ," +
|
||||
"\"PAGE_TO\" INTEGER NOT NULL ," +
|
||||
"\"TIME\" INTEGER NOT NULL );");
|
||||
db.execSQL("INSERT INTO \"QUICK_SEARCH2\" (" +
|
||||
"_id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, PAGE_FROM, PAGE_TO, TIME)" +
|
||||
"SELECT _id, NAME, MODE, CATEGORY, KEYWORD, ADVANCE_SEARCH, MIN_RATING, -1, -1, TIME FROM QUICK_SEARCH;");
|
||||
db.execSQL("DROP TABLE QUICK_SEARCH");
|
||||
db.execSQL("ALTER TABLE QUICK_SEARCH2 RENAME TO QUICK_SEARCH");
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
List<DownloadInfo> list = dao.list();
|
||||
// Fix state
|
||||
for (DownloadInfo info : list) {
|
||||
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
info.state = DownloadInfo.STATE_NONE;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static synchronized void updateDownloadInfo(List<DownloadInfo> downloadInfos) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
dao.update(downloadInfos);
|
||||
}
|
||||
|
||||
// Insert or update
|
||||
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
|
||||
DownloadsDao dao = db.downloadsDao();
|
||||
if (null != dao.load(downloadInfo.getGid())) {
|
||||
// Update
|
||||
dao.update(downloadInfo);
|
||||
} else {
|
||||
// Insert
|
||||
dao.insert(downloadInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
|
||||
db.downloadsDao().delete(downloadInfo);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static synchronized String getDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) {
|
||||
return raw.getDirname();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update
|
||||
*/
|
||||
public static synchronized void putDownloadDirname(long gid, String dirname) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
DownloadDirname raw = dao.load(gid);
|
||||
if (raw != null) { // Update
|
||||
raw.setDirname(dirname);
|
||||
dao.update(raw);
|
||||
} else { // Insert
|
||||
raw = new DownloadDirname();
|
||||
raw.setGid(gid);
|
||||
raw.setDirname(dirname);
|
||||
dao.insert(raw);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadDirname(long gid) {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void clearDownloadDirname() {
|
||||
DownloadDirnameDao dao = db.downloadDirnameDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized DownloadLabel addDownloadLabel(String label) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
DownloadLabel raw = new DownloadLabel();
|
||||
raw.setLabel(label);
|
||||
raw.setTime(System.currentTimeMillis());
|
||||
raw.setId(dao.insert(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
|
||||
// Reset id
|
||||
raw.setId(null);
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
raw.setId(dao.insert(raw));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.update(raw);
|
||||
}
|
||||
|
||||
public static synchronized void moveDownloadLabel(int fromPosition, int toPosition) {
|
||||
if (fromPosition == toPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean reverse = fromPosition > toPosition;
|
||||
int offset = reverse ? toPosition : fromPosition;
|
||||
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
|
||||
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
List<DownloadLabel> list = dao.list(offset, limit);
|
||||
|
||||
int step = reverse ? 1 : -1;
|
||||
int start = reverse ? limit - 1 : 0;
|
||||
int end = reverse ? 0 : limit - 1;
|
||||
long toTime = list.get(end).getTime();
|
||||
for (int i = end; reverse ? i < start : i > start; i += step) {
|
||||
list.get(i).setTime(list.get(i + step).getTime());
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
|
||||
DownloadLabelDao dao = db.downloadLabelDao();
|
||||
dao.delete(raw);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list();
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
List<LocalFavoriteInfo> list = dao.list("%" + query + "%");
|
||||
return new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long gid) {
|
||||
db.localFavoritesDao().deleteByKey(gid);
|
||||
}
|
||||
|
||||
public static synchronized void removeLocalFavorites(long[] gidArray) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
for (long gid : gidArray) {
|
||||
dao.deleteByKey(gid);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean containLocalFavorites(long gid) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
return null != dao.load(gid);
|
||||
}
|
||||
|
||||
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
|
||||
LocalFavoritesDao dao = db.localFavoritesDao();
|
||||
if (null == dao.load(galleryInfo.getGid())) {
|
||||
LocalFavoriteInfo info;
|
||||
if (galleryInfo instanceof LocalFavoriteInfo) {
|
||||
info = (LocalFavoriteInfo) galleryInfo;
|
||||
} else {
|
||||
info = new LocalFavoriteInfo(galleryInfo);
|
||||
info.time = System.currentTimeMillis();
|
||||
}
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putLocalFavorites(List<GalleryInfo> galleryInfoList) {
|
||||
for (GalleryInfo gi : galleryInfoList) {
|
||||
putLocalFavorites(gi);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized List<QuickSearch> getAllQuickSearch() {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
return dao.list();
|
||||
}
|
||||
|
||||
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
quickSearch.setId(null);
|
||||
quickSearch.setTime(System.currentTimeMillis());
|
||||
quickSearch.setId(dao.insert(quickSearch));
|
||||
}
|
||||
|
||||
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
dao.insert(quickSearch);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
dao.delete(quickSearch);
|
||||
}
|
||||
|
||||
public static synchronized void moveQuickSearch(int fromPosition, int toPosition) {
|
||||
if (fromPosition == toPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean reverse = fromPosition > toPosition;
|
||||
int offset = reverse ? toPosition : fromPosition;
|
||||
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
|
||||
|
||||
QuickSearchDao dao = db.quickSearchDao();
|
||||
List<QuickSearch> list = dao.list(offset, limit);
|
||||
|
||||
int step = reverse ? 1 : -1;
|
||||
int start = reverse ? limit - 1 : 0;
|
||||
int end = reverse ? 0 : limit - 1;
|
||||
long toTime = list.get(end).getTime();
|
||||
for (int i = end; reverse ? i < start : i > start; i += step) {
|
||||
list.get(i).setTime(list.get(i + step).getTime());
|
||||
}
|
||||
list.get(start).setTime(toTime);
|
||||
|
||||
dao.update(list);
|
||||
}
|
||||
|
||||
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
|
||||
return db.historyDao().listLazy();
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
HistoryInfo info;
|
||||
if (galleryInfo instanceof HistoryInfo) {
|
||||
info = (HistoryInfo) galleryInfo;
|
||||
} else {
|
||||
info = new HistoryInfo(galleryInfo);
|
||||
}
|
||||
info.time = System.currentTimeMillis();
|
||||
if (null != dao.load(info.getGid())) {
|
||||
dao.update(info);
|
||||
} else {
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfoNonRefresh(GalleryInfo info) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
var i = dao.load(info.getGid());
|
||||
if (null != i) {
|
||||
HistoryInfo historyInfo;
|
||||
if (info instanceof HistoryInfo) {
|
||||
historyInfo = (HistoryInfo) info;
|
||||
} else {
|
||||
historyInfo = new HistoryInfo(info);
|
||||
historyInfo.time = i.time;
|
||||
}
|
||||
dao.update(historyInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
for (HistoryInfo info : historyInfoList) {
|
||||
if (null == dao.load(info.getGid())) {
|
||||
dao.insert(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.delete(info);
|
||||
}
|
||||
|
||||
public static synchronized void clearHistoryInfo() {
|
||||
HistoryDao dao = db.historyDao();
|
||||
dao.deleteAll();
|
||||
}
|
||||
|
||||
public static synchronized List<Filter> getAllFilter() {
|
||||
return db.filterDao().list();
|
||||
}
|
||||
|
||||
public static synchronized boolean addFilter(Filter filter) {
|
||||
Filter existFilter;
|
||||
try {
|
||||
existFilter = db.filterDao().load(filter.text, filter.mode);
|
||||
} catch (Exception e) {
|
||||
existFilter = null;
|
||||
}
|
||||
if (existFilter == null) {
|
||||
filter.setId(null);
|
||||
filter.setId(db.filterDao().insert(filter));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void deleteFilter(Filter filter) {
|
||||
db.filterDao().delete(filter);
|
||||
}
|
||||
|
||||
public static synchronized void triggerFilter(Filter filter) {
|
||||
filter.enable = !filter.enable;
|
||||
db.filterDao().update(filter);
|
||||
}
|
||||
|
||||
private static <T> void copyDao(BasicDao<T> from, BasicDao<T> to) {
|
||||
List<T> list = from.list();
|
||||
for (T item : list)
|
||||
to.insert(item);
|
||||
}
|
||||
|
||||
public static synchronized boolean exportDB(Context context, Uri uri) {
|
||||
final String ehExportName = "eh.export.db";
|
||||
|
||||
// Delete old export db
|
||||
context.deleteDatabase(ehExportName);
|
||||
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
|
||||
|
||||
try {
|
||||
// Copy data to a export db
|
||||
copyDao(db.downloadsDao(), newDb.downloadsDao());
|
||||
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao());
|
||||
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao());
|
||||
copyDao(db.historyDao(), newDb.historyDao());
|
||||
copyDao(db.quickSearchDao(), newDb.quickSearchDao());
|
||||
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao());
|
||||
copyDao(db.filterDao(), newDb.filterDao());
|
||||
|
||||
// Close export db so we can copy it
|
||||
if (newDb.isOpen()) {
|
||||
newDb.close();
|
||||
}
|
||||
|
||||
// Copy export db to data dir
|
||||
File dbFile = context.getDatabasePath(ehExportName);
|
||||
if (dbFile == null || !dbFile.isFile()) {
|
||||
return false;
|
||||
}
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
is = new FileInputStream(dbFile);
|
||||
os = context.getContentResolver().openOutputStream(uri);
|
||||
IOUtils.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
IOUtils.closeQuietly(os);
|
||||
}
|
||||
|
||||
// Delete failed file
|
||||
return false;
|
||||
} finally {
|
||||
context.deleteDatabase(ehExportName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return error string, null for no error
|
||||
*/
|
||||
public static synchronized String importDB(Context context, Uri uri) {
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
// Copy file to cache dir
|
||||
File file = File.createTempFile("importDatabase", "");
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
byte[] buff = new byte[1024];
|
||||
int read;
|
||||
if (inputStream != null) {
|
||||
while ((read = inputStream.read(buff, 0, buff.length)) > 0) {
|
||||
outputStream.write(buff, 0, read);
|
||||
}
|
||||
} else {
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
}
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
|
||||
// Check database version
|
||||
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
|
||||
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
|
||||
int newVersion = CUR_DB_VER;
|
||||
int oldVersion = oldDB.getVersion();
|
||||
if (oldVersion < newVersion) {
|
||||
upgradeDB(oldDB, oldVersion);
|
||||
oldDB.setVersion(newVersion);
|
||||
} else if (oldVersion > newVersion) {
|
||||
return context.getString(R.string.db_version_too_high);
|
||||
}
|
||||
|
||||
// Crete temp room database from cache file
|
||||
String tmpDBName = "tmp.db";
|
||||
context.deleteDatabase(tmpDBName);
|
||||
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
|
||||
.createFromFile(file).allowMainThreadQueries().build();
|
||||
|
||||
// Download label
|
||||
DownloadManager manager = EhApplication.getDownloadManager();
|
||||
try {
|
||||
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
|
||||
manager.addDownloadLabel(downloadLabelList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Downloads
|
||||
try {
|
||||
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
|
||||
manager.addDownload(downloadInfoList, false);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Download dirname
|
||||
try {
|
||||
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
|
||||
for (DownloadDirname dirname : downloadDirnameList) {
|
||||
putDownloadDirname(dirname.getGid(), dirname.getDirname());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// History
|
||||
try {
|
||||
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
|
||||
putHistoryInfo(historyInfoList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
try {
|
||||
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
|
||||
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
|
||||
List<QuickSearch> importList = new ArrayList<>();
|
||||
for (QuickSearch quickSearch : quickSearchList) {
|
||||
String name = quickSearch.getName();
|
||||
for (QuickSearch q : currentQuickSearchList) {
|
||||
if (ObjectUtils.equal(q.getName(), name)) {
|
||||
// The same name
|
||||
name = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (null == name) {
|
||||
continue;
|
||||
}
|
||||
importList.add(quickSearch);
|
||||
}
|
||||
importQuickSearch(importList);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// LocalFavorites
|
||||
try {
|
||||
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
|
||||
for (LocalFavoriteInfo info : localFavoriteInfoList) {
|
||||
putLocalFavorites(info);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Filter
|
||||
try {
|
||||
List<Filter> filterList = oldRoomDatabase.filterDao().list();
|
||||
List<Filter> currentFilterList = db.filterDao().list();
|
||||
for (Filter filter : filterList) {
|
||||
if (!currentFilterList.contains(filter)) {
|
||||
addFilter(filter);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// Delete temp database
|
||||
if (oldRoomDatabase.isOpen()) {
|
||||
oldRoomDatabase.close();
|
||||
}
|
||||
context.deleteDatabase(tmpDBName);
|
||||
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
// Ignore
|
||||
return context.getString(R.string.cant_read_the_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import com.hippo.Native
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ehDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.BasicDao
|
||||
import com.hippo.ehviewer.dao.DownloadDirname
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.DownloadLabel
|
||||
import com.hippo.ehviewer.dao.EhDatabase
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
|
||||
object EhDB {
|
||||
private const val CUR_DB_VER = 4
|
||||
private val db = ehDatabase
|
||||
|
||||
// Fix state
|
||||
@get:Synchronized
|
||||
val allDownloadInfo: List<DownloadInfo>
|
||||
get() = db.downloadsDao().list().onEach {
|
||||
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
it.state = DownloadInfo.STATE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.update(downloadInfos)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
db.downloadsDao().run {
|
||||
if (load(downloadInfo.gid) != null) {
|
||||
update(downloadInfo)
|
||||
} else {
|
||||
insert(downloadInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
db.downloadsDao().delete(downloadInfo)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadDirname(gid: Long): String? {
|
||||
val dao = db.downloadDirnameDao()
|
||||
val raw = dao.load(gid)
|
||||
return raw?.dirname
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putDownloadDirname(gid: Long, dirname: String?) {
|
||||
val dao = db.downloadDirnameDao()
|
||||
var raw = dao.load(gid)
|
||||
if (raw != null) {
|
||||
raw.dirname = dirname
|
||||
dao.update(raw)
|
||||
} else {
|
||||
raw = DownloadDirname(gid, dirname)
|
||||
dao.insert(raw)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadDirname(gid: Long) {
|
||||
val dao = db.downloadDirnameDao()
|
||||
dao.deleteByKey(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearDownloadDirname() {
|
||||
val dao = db.downloadDirnameDao()
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allDownloadLabelList: List<DownloadLabel>
|
||||
get() = db.downloadLabelDao().list()
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadLabel(label: String): DownloadLabel {
|
||||
val dao = db.downloadLabelDao()
|
||||
val raw = DownloadLabel()
|
||||
raw.label = label
|
||||
raw.time = System.currentTimeMillis()
|
||||
raw.id = dao.insert(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||
// Reset id
|
||||
raw.id = null
|
||||
val dao = db.downloadLabelDao()
|
||||
raw.id = dao.insert(raw)
|
||||
return raw
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateDownloadLabel(raw: DownloadLabel?) {
|
||||
val dao = db.downloadLabelDao()
|
||||
dao.update(raw!!)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) {
|
||||
return
|
||||
}
|
||||
val reverse = fromPosition > toPosition
|
||||
val offset = if (reverse) toPosition else fromPosition
|
||||
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||
val dao = db.downloadLabelDao()
|
||||
val list = dao.list(offset, limit)
|
||||
val step = if (reverse) 1 else -1
|
||||
val start = if (reverse) limit - 1 else 0
|
||||
val end = if (reverse) 0 else limit - 1
|
||||
val toTime = list[end].time
|
||||
var i = end
|
||||
while (if (reverse) i < start else i > start) {
|
||||
list[i].time = list[i + step].time
|
||||
i += step
|
||||
}
|
||||
list[start].time = toTime
|
||||
dao.update(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeDownloadLabel(raw: DownloadLabel?) {
|
||||
val dao = db.downloadLabelDao()
|
||||
dao.delete(raw!!)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allLocalFavorites: List<GalleryInfo>
|
||||
get() {
|
||||
val dao = db.localFavoritesDao()
|
||||
val list = dao.list()
|
||||
return ArrayList<GalleryInfo>(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun searchLocalFavorites(query: String): List<GalleryInfo> {
|
||||
val dao = db.localFavoritesDao()
|
||||
val list = dao.list("%$query%")
|
||||
return ArrayList<GalleryInfo>(list)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeLocalFavorites(gid: Long) {
|
||||
db.localFavoritesDao().deleteByKey(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeLocalFavorites(gidArray: LongArray) {
|
||||
val dao = db.localFavoritesDao()
|
||||
for (gid in gidArray) {
|
||||
dao.deleteByKey(gid)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun containLocalFavorites(gid: Long): Boolean {
|
||||
val dao = db.localFavoritesDao()
|
||||
return null != dao.load(gid)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putLocalFavorites(galleryInfo: GalleryInfo) {
|
||||
val dao = db.localFavoritesDao()
|
||||
if (null == dao.load(galleryInfo.gid)) {
|
||||
val info: LocalFavoriteInfo
|
||||
if (galleryInfo is LocalFavoriteInfo) {
|
||||
info = galleryInfo
|
||||
} else {
|
||||
info = LocalFavoriteInfo(galleryInfo)
|
||||
info.time = System.currentTimeMillis()
|
||||
}
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {
|
||||
for (gi in galleryInfoList) {
|
||||
putLocalFavorites(gi)
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allQuickSearch: List<QuickSearch>
|
||||
get() {
|
||||
val dao = db.quickSearchDao()
|
||||
return dao.list()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun insertQuickSearch(quickSearch: QuickSearch) {
|
||||
val dao = db.quickSearchDao()
|
||||
quickSearch.id = null
|
||||
quickSearch.time = System.currentTimeMillis()
|
||||
quickSearch.id = dao.insert(quickSearch)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun importQuickSearch(quickSearchList: List<QuickSearch?>) {
|
||||
val dao = db.quickSearchDao()
|
||||
for (quickSearch in quickSearchList) {
|
||||
dao.insert(quickSearch!!)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteQuickSearch(quickSearch: QuickSearch?) {
|
||||
val dao = db.quickSearchDao()
|
||||
dao.delete(quickSearch)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveQuickSearch(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) {
|
||||
return
|
||||
}
|
||||
val reverse = fromPosition > toPosition
|
||||
val offset = if (reverse) toPosition else fromPosition
|
||||
val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1
|
||||
val dao = db.quickSearchDao()
|
||||
val list = dao.list(offset, limit)
|
||||
val step = if (reverse) 1 else -1
|
||||
val start = if (reverse) limit - 1 else 0
|
||||
val end = if (reverse) 0 else limit - 1
|
||||
val toTime = list[end].time
|
||||
var i = end
|
||||
while (if (reverse) i < start else i > start) {
|
||||
list[i].time = list[i + step].time
|
||||
i += step
|
||||
}
|
||||
list[start].time = toTime
|
||||
dao.update(list)
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val historyLazyList: PagingSource<Int, HistoryInfo>
|
||||
get() = db.historyDao().listLazy()
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfo(galleryInfo: GalleryInfo?) {
|
||||
val dao = db.historyDao()
|
||||
val info: HistoryInfo = if (galleryInfo is HistoryInfo) {
|
||||
galleryInfo
|
||||
} else {
|
||||
HistoryInfo(galleryInfo!!)
|
||||
}
|
||||
info.time = System.currentTimeMillis()
|
||||
if (null != dao.load(info.gid)) {
|
||||
dao.update(info)
|
||||
} else {
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfoNonRefresh(info: GalleryInfo) {
|
||||
val dao = db.historyDao()
|
||||
val i = dao.load(info.gid)
|
||||
if (null != i) {
|
||||
val historyInfo: HistoryInfo
|
||||
if (info is HistoryInfo) {
|
||||
historyInfo = info
|
||||
} else {
|
||||
historyInfo = HistoryInfo(info)
|
||||
historyInfo.time = i.time
|
||||
}
|
||||
dao.update(historyInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {
|
||||
val dao = db.historyDao()
|
||||
for (info in historyInfoList) {
|
||||
if (null == dao.load(info.gid)) {
|
||||
dao.insert(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteHistoryInfo(info: HistoryInfo?) {
|
||||
val dao = db.historyDao()
|
||||
dao.delete(info!!)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearHistoryInfo() {
|
||||
val dao = db.historyDao()
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val allFilter: List<Filter>
|
||||
get() = db.filterDao().list()
|
||||
|
||||
@Synchronized
|
||||
fun addFilter(filter: Filter): Boolean {
|
||||
val existFilter: Filter? = try {
|
||||
db.filterDao().load(filter.text!!, filter.mode)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
return if (existFilter == null) {
|
||||
filter.id = null
|
||||
filter.id = db.filterDao().insert(filter)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteFilter(filter: Filter) {
|
||||
db.filterDao().delete(filter)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerFilter(filter: Filter) {
|
||||
filter.enable = filter.enable?.not() ?: false
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {
|
||||
val list = from.list()
|
||||
for (item in list) to.insert(item)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun exportDB(context: Context, uri: Uri): Boolean {
|
||||
val ehExportName = "eh.export.db"
|
||||
runCatching {
|
||||
context.deleteDatabase(ehExportName)
|
||||
val newDb = databaseBuilder(context, EhDatabase::class.java, ehExportName).build()
|
||||
copyDao(db.downloadsDao(), newDb.downloadsDao())
|
||||
copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())
|
||||
copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())
|
||||
copyDao(db.historyDao(), newDb.historyDao())
|
||||
copyDao(db.quickSearchDao(), newDb.quickSearchDao())
|
||||
copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())
|
||||
copyDao(db.filterDao(), newDb.filterDao())
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
|
||||
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
|
||||
Native.sendfile(fromFd.fd, toFd.fd)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return error string, null for no error
|
||||
*/
|
||||
@Synchronized
|
||||
fun importDB(context: Context, uri: Uri): String? {
|
||||
runCatching {
|
||||
val oldDB = databaseBuilder(context, EhDatabase::class.java, "tmp.db")
|
||||
.createFromInputStream { context.contentResolver.openInputStream(uri) }.build()
|
||||
|
||||
// Download label
|
||||
val manager = DownloadManager
|
||||
runCatching {
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
manager.addDownloadLabel(downloadLabelList)
|
||||
}
|
||||
|
||||
// Downloads
|
||||
runCatching {
|
||||
val downloadInfoList = oldDB.downloadsDao().list()
|
||||
manager.addDownload(downloadInfoList, false)
|
||||
}
|
||||
|
||||
// Download dirname
|
||||
runCatching {
|
||||
oldDB.downloadDirnameDao().list().forEach {
|
||||
putDownloadDirname(it.gid, it.dirname)
|
||||
}
|
||||
}
|
||||
|
||||
// History
|
||||
runCatching {
|
||||
val historyInfoList = oldDB.historyDao().list()
|
||||
putHistoryInfo(historyInfoList)
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
runCatching {
|
||||
val quickSearchList = oldDB.quickSearchDao().list()
|
||||
val currentQuickSearchList = db.quickSearchDao().list()
|
||||
val importList = quickSearchList.mapNotNull { newQS ->
|
||||
newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }
|
||||
}
|
||||
importQuickSearch(importList)
|
||||
}
|
||||
|
||||
// LocalFavorites
|
||||
runCatching {
|
||||
oldDB.localFavoritesDao().list().forEach {
|
||||
putLocalFavorites(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter
|
||||
runCatching {
|
||||
val filterList = oldDB.filterDao().list()
|
||||
val currentFilterList = db.filterDao().list()
|
||||
filterList.forEach {
|
||||
if (it !in currentFilterList) addFilter(it)
|
||||
}
|
||||
}
|
||||
oldDB.close()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return context.getString(R.string.cant_read_the_file)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder;
|
||||
import com.hippo.ehviewer.ui.CommonOperations;
|
||||
import com.hippo.ehviewer.ui.scene.GalleryListScene;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.AssertUtilsKt;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -63,6 +63,9 @@ public class Settings {
|
||||
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
|
||||
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
|
||||
public static final String KEY_REQUEST_NEWS = "request_news";
|
||||
public static final String KEY_REQUEST_NEWS_TIMER = "request_news_timer";
|
||||
public static final String KEY_REQUEST_NEWS_TIMER_HOUR = "request_news_timer_hour";
|
||||
public static final String KEY_REQUEST_NEWS_TIMER_MINUTE = "request_news_timer_minute";
|
||||
public static final String KEY_HIDE_HV_EVENTS = "hide_hv_events";
|
||||
public static final String[] SIGN_IN_REQUIRED = {
|
||||
KEY_IMAGE_LIMITS,
|
||||
@@ -71,6 +74,7 @@ public class Settings {
|
||||
KEY_MY_TAGS,
|
||||
KEY_SHOW_JPN_TITLE,
|
||||
KEY_REQUEST_NEWS,
|
||||
KEY_REQUEST_NEWS_TIMER,
|
||||
KEY_HIDE_HV_EVENTS
|
||||
};
|
||||
/********************
|
||||
@@ -103,7 +107,7 @@ public class Settings {
|
||||
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
|
||||
public static final String KEY_BYPASS_VPN = "bypass_vpn";
|
||||
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = true;
|
||||
private static final boolean DEFAULT_HIDE_HV_EVENTS = false;
|
||||
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
|
||||
private static final boolean DEFAULT_SHOW_COMMENTS = true;
|
||||
private static final String TAG = Settings.class.getSimpleName();
|
||||
@@ -205,7 +209,7 @@ public class Settings {
|
||||
private static final int DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0;
|
||||
private static final String KEY_DOWNLOAD_DELAY = "download_delay";
|
||||
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = true;
|
||||
private static final boolean DEFAULT_REQUEST_NEWS = false;
|
||||
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
|
||||
private static final String KEY_QS_SAVE_PROGRESS = "qs_save_progress";
|
||||
private static final boolean DEFAULT_QS_SAVE_PROGRESS = true;
|
||||
@@ -562,7 +566,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static void putFavCat(String[] value) {
|
||||
AssertUtils.assertEquals(10, value.length);
|
||||
AssertUtilsKt.assertEquals(10, value.length);
|
||||
sSettingsPre.edit()
|
||||
.putString(KEY_FAV_CAT_0, value[0])
|
||||
.putString(KEY_FAV_CAT_1, value[1])
|
||||
@@ -593,7 +597,7 @@ public class Settings {
|
||||
}
|
||||
|
||||
public static void putFavCount(int[] count) {
|
||||
AssertUtils.assertEquals(10, count.length);
|
||||
AssertUtilsKt.assertEquals(10, count.length);
|
||||
sSettingsPre.edit()
|
||||
.putInt(KEY_FAV_COUNT_0, count[0])
|
||||
.putInt(KEY_FAV_COUNT_1, count[1])
|
||||
|
||||
@@ -183,7 +183,10 @@ object EhCookieStore : CookieJar {
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
for (cookie in cookies) {
|
||||
addCookie(cookie)
|
||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||
if (cookie.name != "__utmp") {
|
||||
addCookie(cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import android.util.Log
|
||||
import android.util.Pair
|
||||
import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -50,7 +49,6 @@ import com.hippo.ehviewer.client.parser.VoteCommentParser
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.network.StatusCodeException
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.AssertUtils
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -322,10 +320,6 @@ object EhEngine {
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
val html = EventPaneParser.parse(body)
|
||||
if (html != null) {
|
||||
application.showEventPane(html)
|
||||
}
|
||||
return GalleryDetailParser.parse(body!!)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -566,7 +560,7 @@ object EhEngine {
|
||||
gidArray: LongArray,
|
||||
tokenArray: Array<String?>, dstCat: Int
|
||||
): Void? {
|
||||
AssertUtils.assertEquals(gidArray.size, tokenArray.size)
|
||||
check(gidArray.size == tokenArray.size)
|
||||
var i = 0
|
||||
val n = gidArray.size
|
||||
while (i < n) {
|
||||
@@ -847,6 +841,30 @@ object EhEngine {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
suspend fun getNews(parse: Boolean): String? {
|
||||
val url = EhUrl.URL_NEWS
|
||||
val referer = EhUrl.REFERER_E
|
||||
Log.d(TAG, url)
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
val call = okHttpClient.newCall(request)
|
||||
var body: String? = null
|
||||
var headers: Headers? = null
|
||||
var code = -1
|
||||
try {
|
||||
call.executeAsync().use { response ->
|
||||
code = response.code
|
||||
headers = response.headers
|
||||
body = response.body.string()
|
||||
return if (parse) EventPaneParser.parse(body!!) else null
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
transformException(code, headers, body, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
private suspend fun getProfileInternal(
|
||||
url: String, referer: String
|
||||
|
||||
@@ -39,7 +39,7 @@ object EhFilter {
|
||||
private val TAG = EhFilter::class.java.simpleName
|
||||
|
||||
init {
|
||||
val list = EhDB.getAllFilter()
|
||||
val list = EhDB.allFilter
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
@@ -112,7 +112,7 @@ object EhFilter {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerFilter(filter: Filter?) {
|
||||
fun triggerFilter(filter: Filter) {
|
||||
EhDB.triggerFilter(filter)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,9 @@ import com.hippo.ehviewer.AppConfig
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.HashCodeUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import com.hippo.yorozuya.copyToFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -37,7 +36,6 @@ import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
@@ -197,22 +195,21 @@ object EhTagDatabase {
|
||||
private fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
||||
val request: Request = Request.Builder().url(url).build()
|
||||
val call = client.newCall(request)
|
||||
try {
|
||||
runCatching {
|
||||
call.execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return false
|
||||
}
|
||||
val body = response.body
|
||||
body.byteStream()
|
||||
.use { ins -> FileOutputStream(file).use { os -> IOUtils.copy(ins, os) } }
|
||||
response.body.use {
|
||||
it.copyToFile(file)
|
||||
}
|
||||
return true
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
FileUtils.delete(file)
|
||||
t.printStackTrace()
|
||||
ExceptionUtils.throwIfFatal(t)
|
||||
return false
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
it.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
@@ -46,6 +46,7 @@ object EhUrl {
|
||||
const val URL_FORUMS = "https://forums.e-hentai.org/"
|
||||
const val URL_FUNDS = HOST_E + "exchange.php?t=gp"
|
||||
const val URL_HOME = HOST_E + "home.php"
|
||||
const val URL_NEWS = HOST_E + "news.php"
|
||||
const val REFERER_EX = "https://$DOMAIN_EX"
|
||||
const val ORIGIN_EX = REFERER_EX
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
|
||||
+5
-24
@@ -15,31 +15,12 @@
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
public class EventPaneParser {
|
||||
|
||||
public static String parse(String body) {
|
||||
String event = null;
|
||||
try {
|
||||
Document d = Jsoup.parse(body);
|
||||
Element eventpane = d.getElementById("eventpane");
|
||||
if (eventpane != null) {
|
||||
event = eventpane.html();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return event;
|
||||
object EventPaneParser {
|
||||
fun parse(body: String): String? {
|
||||
return Jsoup.parse(body).getElementById("eventpane")?.html()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.util.JsoupUtils;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.AssertUtilsKt;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
@@ -46,7 +46,7 @@ public class FavoritesParser {
|
||||
//noinspection ConstantConditions
|
||||
Elements fps = ido.getElementsByClass("fp");
|
||||
// Last one is "fp fps"
|
||||
AssertUtils.assertEquals(11, fps.size());
|
||||
AssertUtilsKt.assertEquals(11, fps.size());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Element fp = fps.get(i);
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.JsoupUtils
|
||||
import com.hippo.yorozuya.NumberUtils
|
||||
import com.hippo.yorozuya.StringUtils
|
||||
import com.hippo.yorozuya.trimAnd
|
||||
import com.hippo.yorozuya.unescapeXml
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
@@ -91,7 +92,8 @@ object GalleryDetailParser {
|
||||
// Error info
|
||||
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
||||
val galleryDetail = GalleryDetail()
|
||||
val document = Jsoup.parse(body)
|
||||
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
|
||||
val document = Jsoup.parse(body.replace("del>", "s>"))
|
||||
parseDetail(galleryDetail, document, body)
|
||||
galleryDetail.tags = parseTagGroups(document)
|
||||
galleryDetail.comments = parseComments(document)
|
||||
@@ -336,7 +338,7 @@ object GalleryDetailParser {
|
||||
// Id
|
||||
val a = element.previousElementSibling()
|
||||
val name = a!!.attr("name")
|
||||
comment.id = StringUtils.trim(name).substring(1).toInt().toLong()
|
||||
comment.id = name trimAnd { substring(1).toInt().toLong() }
|
||||
// Editable, vote up and vote down
|
||||
val c4 = JsoupUtils.getElementByClass(element, "c4")
|
||||
if (null != c4) {
|
||||
@@ -376,15 +378,15 @@ object GalleryDetailParser {
|
||||
}
|
||||
// time
|
||||
val c3 = JsoupUtils.getElementByClass(element, "c3")
|
||||
var temp = c3!!.ownText()
|
||||
if (temp.endsWith(':')) {
|
||||
temp = temp.substring("Posted on ".length, temp.length - " by:".length)
|
||||
val temp = c3!!.ownText()
|
||||
val time = if (temp.endsWith(':')) {
|
||||
// user
|
||||
comment.user = c3.child(0).text()
|
||||
temp.substring("Posted on ".length, temp.length - " by:".length)
|
||||
} else {
|
||||
temp = temp.substring("Posted on ".length)
|
||||
temp.substring("Posted on ".length)
|
||||
}
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
|
||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
|
||||
// comment
|
||||
val c6 = JsoupUtils.getElementByClass(element, "c6")
|
||||
// fix underline support
|
||||
@@ -407,7 +409,7 @@ object GalleryDetailParser {
|
||||
val e = c8.children().first()
|
||||
if (e != null) {
|
||||
comment.lastEdited =
|
||||
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli()
|
||||
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
|
||||
}
|
||||
}
|
||||
comment
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil.disk.DiskCache
|
||||
|
||||
inline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {
|
||||
val editor = edit(key) ?: return false
|
||||
editor.runCatching {
|
||||
block(this)
|
||||
}.onFailure {
|
||||
editor.abort()
|
||||
throw it
|
||||
}.onSuccess {
|
||||
editor.commit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {
|
||||
(get(key) ?: return false).use { block(it) }
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.hippo.ehviewer.dailycheck
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.Html
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
private val signedIn
|
||||
get() = EhApplication.ehCookieStore.hasSignedIn()
|
||||
|
||||
private const val CHANNEL_ID = "DailyCheckNotification"
|
||||
|
||||
class DailyCheckWork(val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result = withIOContext {
|
||||
runCatching {
|
||||
if (signedIn) {
|
||||
EhEngine.getNews(true)?.let { showEventPane(it) }
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
return@withIOContext Result.retry()
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun showEventPane(html: String) {
|
||||
if (Settings.getHideHvEvents() && html.contains("You have encountered a monster!")) {
|
||||
return
|
||||
}
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val chan = NotificationChannelCompat
|
||||
.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(CHANNEL_ID)
|
||||
.build()
|
||||
notificationManager.createNotificationChannel(chan)
|
||||
val msg = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_monochrome)
|
||||
.setContentText(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.build()
|
||||
runCatching {
|
||||
notificationManager.notify(1, msg)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val schedHour
|
||||
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, -1).takeUnless { it == -1 }
|
||||
|
||||
val schedMinute
|
||||
get() = Settings.getInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, -1).takeUnless { it == -1 }
|
||||
|
||||
private val whenToWork
|
||||
get() = LocalDateTime.now()
|
||||
.run { withHour(schedHour ?: hour) }
|
||||
.run { withMinute(schedMinute ?: minute) }
|
||||
|
||||
private val initialDelay
|
||||
get() = Duration.between(LocalDateTime.now(), whenToWork)
|
||||
.run { if (isNegative) plusDays(1) else this }
|
||||
|
||||
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(initialDelay)
|
||||
.build()
|
||||
}
|
||||
|
||||
private const val workName = "DailyCheckWork"
|
||||
|
||||
fun updateDailyCheckWork(context: Context) {
|
||||
if (Settings.getRequestNews()) {
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
workName,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
getDailyCheckWorkRequest()
|
||||
)
|
||||
} else {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(workName)
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,11 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.AsyncTask
|
||||
import android.util.Log
|
||||
import android.util.SparseLongArray
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.keyIterator
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.DownloadLabel
|
||||
@@ -31,16 +29,17 @@ import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
import com.hippo.ehviewer.spider.write
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.util.IoThreadPoolExecutor
|
||||
import com.hippo.yorozuya.ConcurrentPool
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import com.hippo.yorozuya.ObjectUtils
|
||||
import com.hippo.yorozuya.SimpleHandler
|
||||
import com.hippo.yorozuya.collect.LongList
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.LinkedList
|
||||
import kotlin.contracts.contract
|
||||
|
||||
class DownloadManager : OnSpiderListener {
|
||||
object DownloadManager : OnSpiderListener {
|
||||
// All download info list
|
||||
private val mAllInfoList: LinkedList<DownloadInfo>
|
||||
|
||||
@@ -68,7 +67,7 @@ class DownloadManager : OnSpiderListener {
|
||||
init {
|
||||
|
||||
// Get all labels
|
||||
val labels = EhDB.getAllDownloadLabelList()
|
||||
val labels = EhDB.allDownloadLabelList.toMutableList()
|
||||
mLabelList = labels
|
||||
|
||||
// Create list for each label
|
||||
@@ -82,7 +81,7 @@ class DownloadManager : OnSpiderListener {
|
||||
mDefaultInfoList = LinkedList()
|
||||
|
||||
// Get all info
|
||||
val allInfoList = EhDB.getAllDownloadInfo()
|
||||
val allInfoList = EhDB.allDownloadInfo
|
||||
mAllInfoList = LinkedList(allInfoList)
|
||||
|
||||
// Create all info map
|
||||
@@ -94,14 +93,21 @@ class DownloadManager : OnSpiderListener {
|
||||
allInfoMap.put(info.gid, info)
|
||||
|
||||
// Add to each label list
|
||||
var list = getInfoListForLabel(info.label)
|
||||
if (list == null) {
|
||||
// Can't find the label in label list
|
||||
list = LinkedList()
|
||||
map[info.label] = list
|
||||
if (!containLabel(info.label)) {
|
||||
val label = info.label
|
||||
|
||||
val list = getInfoListForLabel(label) ?: LinkedList<DownloadInfo>().also {
|
||||
/* Contracts says if [getInfoListForLabel] return null, the param [label] is not null
|
||||
* [label] is local immutable
|
||||
* Why [label] is not smart casted to [String!] here ???
|
||||
*
|
||||
* TODO: Report it to kotlin compiler team
|
||||
* https://youtrack.jetbrains.com/issue/KT-56869
|
||||
*/
|
||||
label!!
|
||||
map[info.label] = it
|
||||
if (!containLabel(label)) {
|
||||
// Add label to DB and list
|
||||
labels.add(EhDB.addDownloadLabel(info.label))
|
||||
labels.add(EhDB.addDownloadLabel(label))
|
||||
}
|
||||
}
|
||||
list.add(info)
|
||||
@@ -112,6 +118,9 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
private fun getInfoListForLabel(label: String?): LinkedList<DownloadInfo>? {
|
||||
contract {
|
||||
returns(null) implies (label != null)
|
||||
}
|
||||
return if (label == null) {
|
||||
mDefaultInfoList
|
||||
} else {
|
||||
@@ -344,7 +353,7 @@ class 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)
|
||||
@@ -564,35 +573,25 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
fun resetAllReadingProgress() {
|
||||
val list = LinkedList(mAllInfoList)
|
||||
object : AsyncTask<Void?, Void?, Void?>() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun doInBackground(vararg voids: Void?): Void? {
|
||||
val galleryInfo: GalleryInfo = BaseGalleryInfo()
|
||||
for (downloadInfo in list) {
|
||||
galleryInfo.gid = downloadInfo.gid
|
||||
galleryInfo.token = downloadInfo.token
|
||||
galleryInfo.title = downloadInfo.title
|
||||
galleryInfo.thumb = downloadInfo.thumb
|
||||
galleryInfo.category = downloadInfo.category
|
||||
galleryInfo.posted = downloadInfo.posted
|
||||
galleryInfo.uploader = downloadInfo.uploader
|
||||
galleryInfo.rating = downloadInfo.rating
|
||||
val downloadDir = SpiderDen.getGalleryDownloadDir(galleryInfo.gid) ?: continue
|
||||
val file = downloadDir.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: continue
|
||||
val spiderInfo = readCompatFromUniFile(file) ?: continue
|
||||
spiderInfo.startPage = 0
|
||||
try {
|
||||
spiderInfo.write(file)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Can't write SpiderInfo", e)
|
||||
}
|
||||
suspend fun resetAllReadingProgress() = coroutineScope {
|
||||
mAllInfoMap.keyIterator().forEach { gid ->
|
||||
launch {
|
||||
runCatching {
|
||||
resetReadingProgress(gid)
|
||||
Log.d(TAG, "Current thread: ${Thread.currentThread().name}")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Can't write SpiderInfo", it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}.executeOnExecutor(IoThreadPoolExecutor.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetReadingProgress(gid: Long) {
|
||||
val downloadDir = SpiderDen.getGalleryDownloadDir(gid) ?: return
|
||||
val file = downloadDir.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return
|
||||
val spiderInfo = readCompatFromUniFile(file) ?: return
|
||||
spiderInfo.startPage = 0
|
||||
spiderInfo.write(file)
|
||||
}
|
||||
|
||||
// Update in DB
|
||||
@@ -848,7 +847,7 @@ class DownloadManager : OnSpiderListener {
|
||||
|
||||
override fun onPageFailure(
|
||||
index: Int,
|
||||
error: String,
|
||||
error: String?,
|
||||
finished: Int,
|
||||
downloaded: Int,
|
||||
total: Int
|
||||
@@ -870,11 +869,11 @@ class 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
|
||||
}
|
||||
|
||||
@@ -948,7 +947,7 @@ class DownloadManager : OnSpiderListener {
|
||||
fun onCancel(info: DownloadInfo)
|
||||
}
|
||||
|
||||
private inner class NotifyTask : Runnable {
|
||||
private class NotifyTask : Runnable {
|
||||
private var mType = 0
|
||||
private var mPages = 0
|
||||
private var mIndex = 0
|
||||
@@ -960,12 +959,12 @@ class DownloadManager : OnSpiderListener {
|
||||
private var mDownloaded = 0
|
||||
private var mTotal = 0
|
||||
fun setOnGetPagesData(pages: Int) {
|
||||
mType = Companion.TYPE_ON_GET_PAGES
|
||||
mType = TYPE_ON_GET_PAGES
|
||||
mPages = pages
|
||||
}
|
||||
|
||||
fun setOnGet509Data(index: Int) {
|
||||
mType = Companion.TYPE_ON_GET_509
|
||||
mType = TYPE_ON_GET_509
|
||||
mIndex = index
|
||||
}
|
||||
|
||||
@@ -975,7 +974,7 @@ class DownloadManager : OnSpiderListener {
|
||||
receivedSize: Long,
|
||||
bytesRead: Int
|
||||
) {
|
||||
mType = Companion.TYPE_ON_PAGE_DOWNLOAD
|
||||
mType = TYPE_ON_PAGE_DOWNLOAD
|
||||
mIndex = index
|
||||
mContentLength = contentLength
|
||||
mReceivedSize = receivedSize
|
||||
@@ -983,7 +982,7 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
fun setOnPageSuccessData(index: Int, finished: Int, downloaded: Int, total: Int) {
|
||||
mType = Companion.TYPE_ON_PAGE_SUCCESS
|
||||
mType = TYPE_ON_PAGE_SUCCESS
|
||||
mIndex = index
|
||||
mFinished = finished
|
||||
mDownloaded = downloaded
|
||||
@@ -997,7 +996,7 @@ class DownloadManager : OnSpiderListener {
|
||||
downloaded: Int,
|
||||
total: Int
|
||||
) {
|
||||
mType = Companion.TYPE_ON_PAGE_FAILURE
|
||||
mType = TYPE_ON_PAGE_FAILURE
|
||||
mIndex = index
|
||||
mError = error
|
||||
mFinished = finished
|
||||
@@ -1006,7 +1005,7 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
|
||||
fun setOnFinishDate(finished: Int, downloaded: Int, total: Int) {
|
||||
mType = Companion.TYPE_ON_FINISH
|
||||
mType = TYPE_ON_FINISH
|
||||
mFinished = finished
|
||||
mDownloaded = downloaded
|
||||
mTotal = total
|
||||
@@ -1014,7 +1013,7 @@ class DownloadManager : OnSpiderListener {
|
||||
|
||||
override fun run() {
|
||||
when (mType) {
|
||||
Companion.TYPE_ON_GET_PAGES -> {
|
||||
TYPE_ON_GET_PAGES -> {
|
||||
val info = mCurrentTask
|
||||
if (info == null) {
|
||||
Log.e(TAG, "Current task is null, but it should not be")
|
||||
@@ -1029,20 +1028,20 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
Companion.TYPE_ON_GET_509 -> {
|
||||
TYPE_ON_GET_509 -> {
|
||||
if (mDownloadListener != null) {
|
||||
mDownloadListener!!.onGet509()
|
||||
}
|
||||
}
|
||||
|
||||
Companion.TYPE_ON_PAGE_DOWNLOAD -> mSpeedReminder.onDownload(
|
||||
TYPE_ON_PAGE_DOWNLOAD -> mSpeedReminder.onDownload(
|
||||
mIndex,
|
||||
mContentLength,
|
||||
mReceivedSize,
|
||||
mBytesRead
|
||||
)
|
||||
|
||||
Companion.TYPE_ON_PAGE_SUCCESS -> {
|
||||
TYPE_ON_PAGE_SUCCESS -> {
|
||||
mSpeedReminder.onDone(mIndex)
|
||||
val info = mCurrentTask
|
||||
if (info == null) {
|
||||
@@ -1063,7 +1062,7 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
Companion.TYPE_ON_PAGE_FAILURE -> {
|
||||
TYPE_ON_PAGE_FAILURE -> {
|
||||
mSpeedReminder.onDone(mIndex)
|
||||
val info = mCurrentTask
|
||||
if (info == null) {
|
||||
@@ -1081,7 +1080,7 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
Companion.TYPE_ON_FINISH -> {
|
||||
TYPE_ON_FINISH -> {
|
||||
mSpeedReminder.onFinish()
|
||||
// Download done
|
||||
val info = mCurrentTask
|
||||
@@ -1090,7 +1089,7 @@ class DownloadManager : OnSpiderListener {
|
||||
mCurrentSpider = null
|
||||
// Release spider
|
||||
if (spider != null) {
|
||||
spider.removeOnSpiderListener(this@DownloadManager)
|
||||
spider.removeOnSpiderListener(DownloadManager)
|
||||
SpiderQueen.releaseSpiderQueen(spider, SpiderQueen.MODE_DOWNLOAD)
|
||||
}
|
||||
// Check null
|
||||
@@ -1130,7 +1129,7 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class SpeedReminder : Runnable {
|
||||
internal class SpeedReminder : Runnable {
|
||||
private val mContentLengthMap = SparseLongArray()
|
||||
private val mReceivedSizeMap = SparseLongArray()
|
||||
private var mStop = true
|
||||
@@ -1219,17 +1218,15 @@ class DownloadManager : OnSpiderListener {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DownloadManager::class.java.simpleName
|
||||
private const val TYPE_ON_GET_PAGES = 0
|
||||
private const val TYPE_ON_GET_509 = 1
|
||||
private const val TYPE_ON_PAGE_DOWNLOAD = 2
|
||||
private const val TYPE_ON_PAGE_SUCCESS = 3
|
||||
private const val TYPE_ON_PAGE_FAILURE = 4
|
||||
private const val TYPE_ON_FINISH = 5
|
||||
private val TAG = DownloadManager::class.java.simpleName
|
||||
private const val TYPE_ON_GET_PAGES = 0
|
||||
private const val TYPE_ON_GET_509 = 1
|
||||
private const val TYPE_ON_PAGE_DOWNLOAD = 2
|
||||
private const val TYPE_ON_PAGE_SUCCESS = 3
|
||||
private const val TYPE_ON_PAGE_FAILURE = 4
|
||||
private const val TYPE_ON_FINISH = 5
|
||||
|
||||
fun MutableList<DownloadInfo>.sortByDateDescending() {
|
||||
sortByDescending { it.time }
|
||||
}
|
||||
private fun MutableList<DownloadInfo>.sortByDateDescending() {
|
||||
sortByDescending { it.time }
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
@@ -66,7 +65,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
|
||||
.setName(getString(R.string.download_service))
|
||||
.build()
|
||||
)
|
||||
mDownloadManager = EhApplication.downloadManager
|
||||
mDownloadManager = DownloadManager
|
||||
mDownloadManager!!.setDownloadListener(this)
|
||||
ensureDownloadingBuilder()
|
||||
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.Native;
|
||||
import com.hippo.UriArchiveAccessor;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.FileUtils;
|
||||
import com.hippo.yorozuya.thread.PVLock;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import kotlin.Pair;
|
||||
|
||||
public class ArchivePageLoader extends PageLoader2 {
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
public static Handler showPasswd;
|
||||
public static String passwd;
|
||||
public static PVLock pv = new PVLock(0);
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final LinkedHashMap<Integer, Image.CloseableSource> streams = new LinkedHashMap<>();
|
||||
private final Thread[] decodeThread = new Thread[]{
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask()),
|
||||
new Thread(new DecodeTask())
|
||||
};
|
||||
private Thread archiveThread;
|
||||
private volatile int size = PageLoader.STATE_WAIT;
|
||||
private String error;
|
||||
|
||||
public ArchivePageLoader(Context context, Uri uri) {
|
||||
UriArchiveAccessor archiveAccessor1;
|
||||
try {
|
||||
archiveAccessor1 = new UriArchiveAccessor(context, uri);
|
||||
} catch (Exception e) {
|
||||
archiveAccessor1 = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
archiveAccessor = archiveAccessor1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
int id = sIdGenerator.incrementAndGet();
|
||||
|
||||
archiveThread = new PriorityThread(
|
||||
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
|
||||
archiveThread.start();
|
||||
for (Thread i : decodeThread) {
|
||||
i.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (archiveThread != null) {
|
||||
archiveThread.interrupt();
|
||||
archiveThread = null;
|
||||
}
|
||||
for (Thread i : decodeThread) {
|
||||
i.interrupt();
|
||||
}
|
||||
try {
|
||||
archiveAccessor.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
boolean inDecodeTask;
|
||||
synchronized (streams) {
|
||||
inDecodeTask = streams.containsKey(index);
|
||||
}
|
||||
|
||||
synchronized (requests) {
|
||||
boolean inArchiveTask = requests.contains(index);
|
||||
if (!inArchiveTask && !inDecodeTask) {
|
||||
requests.add(index);
|
||||
requests.notify();
|
||||
}
|
||||
}
|
||||
notifyPageWait(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceRequest(int index) {
|
||||
onRequest(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelRequest(int index) {
|
||||
synchronized (requests) {
|
||||
requests.remove(Integer.valueOf(index));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilename(int index) {
|
||||
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilenameWithExtension(int index) {
|
||||
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
int fd;
|
||||
FileOutputStream stream;
|
||||
try {
|
||||
stream = (FileOutputStream) file.openOutputStream();
|
||||
fd = Native.getFd(stream.getFD());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
archiveAccessor.extractToFd(index, fd);
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
String extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index));
|
||||
UniFile dst = dir.subFile(null != extension ? filename + "." + extension : filename);
|
||||
save(index, dst);
|
||||
return dst;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
|
||||
|
||||
}
|
||||
|
||||
private class ArchiveHostTask implements Runnable {
|
||||
public final Thread[] archiveThreads = new Thread[]{
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask()),
|
||||
new Thread(new ArchiveTask())
|
||||
};
|
||||
|
||||
private void waitPasswd() throws InterruptedException {
|
||||
showPasswd.sendEmptyMessage(0);
|
||||
pv.p();
|
||||
}
|
||||
|
||||
private void notifyError() {
|
||||
showPasswd.sendEmptyMessage(1);
|
||||
}
|
||||
|
||||
private void notifyDismiss() {
|
||||
showPasswd.sendEmptyMessage(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
size = archiveAccessor.open();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
size = 0;
|
||||
}
|
||||
if (size <= 0) {
|
||||
size = PageLoader.STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
// Update size and notify changed
|
||||
notifyDataChanged();
|
||||
|
||||
if (archiveAccessor.needPassword()) {
|
||||
boolean need_request = true;
|
||||
Set<String> set = Settings.getArchivePasswds();
|
||||
if (set != null) {
|
||||
for (String passwd : set) {
|
||||
if (archiveAccessor.providePassword(passwd)) {
|
||||
need_request = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!Thread.currentThread().isInterrupted() && need_request) {
|
||||
try {
|
||||
waitPasswd();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
if (!archiveAccessor.providePassword(passwd))
|
||||
notifyError();
|
||||
else {
|
||||
Settings.putPasswdToArchivePasswds(passwd);
|
||||
notifyDismiss();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Thread i : archiveThreads) {
|
||||
i.start();
|
||||
}
|
||||
|
||||
Object o = new Object();
|
||||
synchronized (o) {
|
||||
try {
|
||||
o.wait();
|
||||
} catch (InterruptedException e) {
|
||||
for (Thread i : archiveThreads) {
|
||||
i.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ArchiveTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
synchronized (requests) {
|
||||
if (requests.isEmpty()) {
|
||||
try {
|
||||
requests.wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Interrupted
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
index = requests.pop();
|
||||
}
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= size) {
|
||||
notifyPageFailed(index, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
synchronized (streams) {
|
||||
if (streams.get(index) != null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Image.CloseableSource src = archiveAccessor.getImageSource(index);
|
||||
|
||||
synchronized (streams) {
|
||||
streams.put(index, src);
|
||||
streams.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DecodeTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
int index;
|
||||
Image.CloseableSource src;
|
||||
synchronized (streams) {
|
||||
if (streams.isEmpty()) {
|
||||
try {
|
||||
streams.wait();
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<Integer, Image.CloseableSource>> iterator = streams.entrySet().iterator();
|
||||
Map.Entry<Integer, Image.CloseableSource> entry = iterator.next();
|
||||
iterator.remove();
|
||||
index = entry.getKey();
|
||||
src = entry.getValue();
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if (src != null) {
|
||||
image = Image.decode(src);
|
||||
}
|
||||
if (image != null) {
|
||||
notifyPageSucceed(index, image);
|
||||
} else {
|
||||
notifyPageFailed(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2023 Tarsin Norbin
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.hippo.Native.getFd
|
||||
import com.hippo.UriArchiveAccessor
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class ArchivePageLoader(context: Context, uri: Uri, passwdFlow: Flow<String>) : PageLoader2(),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val archiveAccessor by lazy { UriArchiveAccessor(context, uri) }
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
size = archiveAccessor.open()
|
||||
if (size == 0) {
|
||||
return@launch
|
||||
}
|
||||
archiveAccessor.run {
|
||||
if (needPassword()) {
|
||||
Settings.getArchivePasswds()?.forEach {
|
||||
if (providePassword(it)) return@launch
|
||||
}
|
||||
passwdFlow.collect {
|
||||
if (providePassword(it)) {
|
||||
Settings.putPasswdToArchivePasswds(it)
|
||||
currentCoroutineContext().cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var size = 0
|
||||
private set
|
||||
|
||||
override fun start() {
|
||||
hostJob.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
cancel()
|
||||
archiveAccessor.close()
|
||||
super.stop()
|
||||
}
|
||||
|
||||
private val mJobMap = hashMapOf<Int, Job>()
|
||||
private val mWorkerMutex by lazy { (0 until size).map { Mutex() } }
|
||||
private val mSemaphore = Semaphore(4)
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
synchronized(mJobMap) {
|
||||
val current = mJobMap[index]
|
||||
if (current?.isActive != true) {
|
||||
mJobMap[index] = launch {
|
||||
mWorkerMutex[index].withLock {
|
||||
mSemaphore.withPermit {
|
||||
doRealWork(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val src = archiveAccessor.getImageSource(index) ?: return
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyPageSucceed(index, image)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int) {
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
hostJob.join()
|
||||
return size != 0
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = size != 0
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mJobMap[index]?.cancel()
|
||||
}
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
return FileUtils.sanitizeFilename(archiveAccessor.getFilename(index))
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
val fd: Int
|
||||
val stream: FileOutputStream
|
||||
try {
|
||||
stream = file.openOutputStream() as FileOutputStream
|
||||
fd = getFd(stream.fd)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
archiveAccessor.extractToFd(index, fd)
|
||||
try {
|
||||
stream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile {
|
||||
val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index))
|
||||
val dst = dir.subFile(if (null != extension) "$filename.$extension" else filename)
|
||||
save(index, dst!!)
|
||||
return dst
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.spider.SpiderQueen;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import kotlin.Pair;
|
||||
|
||||
public class EhPageLoader extends PageLoader2 implements SpiderQueen.OnSpiderListener {
|
||||
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
@Nullable
|
||||
private SpiderQueen mSpiderQueen;
|
||||
|
||||
public EhPageLoader(GalleryInfo galleryInfo) {
|
||||
mGalleryInfo = galleryInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mSpiderQueen = SpiderQueen.obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen.addOnSpiderListener(this);
|
||||
if (mSpiderQueen.size() > 0)
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.removeOnSpiderListener(this);
|
||||
// Activity recreate may called, so wait 3000s
|
||||
SimpleHandler.getInstance().postDelayed(new ReleaseTask(mSpiderQueen), 3000);
|
||||
mSpiderQueen = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartPage() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.getStartPage();
|
||||
} else {
|
||||
return super.getStartPage();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilename(int index) {
|
||||
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getImageFilenameWithExtension(int index) {
|
||||
if (null != mSpiderQueen) {
|
||||
String extension = mSpiderQueen.getExtension(index);
|
||||
if (extension != null) {
|
||||
return String.format(Locale.US, "%d-%s-%08d.%s", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1, extension);
|
||||
}
|
||||
}
|
||||
return String.format(Locale.US, "%d-%s-%08d", mGalleryInfo.getGid(), mGalleryInfo.getToken(), index + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
if (null != mSpiderQueen) {
|
||||
return mSpiderQueen.save(index, file);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
if (null != mSpiderQueen) {
|
||||
return mSpiderQueen.save(index, dir, filename);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putStartPage(int page) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.putStartPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.size();
|
||||
} else {
|
||||
return PageLoader.STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.request(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.forceRequest(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelRequest(int index) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.cancelRequest(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
if (mSpiderQueen != null) {
|
||||
return mSpiderQueen.getError();
|
||||
} else {
|
||||
return "Error"; // TODO
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetPages(int pages) {
|
||||
notifyDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGet509(int index) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, (float) receivedSize / contentLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSuccess(int index, int finished, int downloaded, int total) {}
|
||||
|
||||
@Override
|
||||
public void onPageFailure(int index, String error, int finished, int downloaded, int total) {
|
||||
notifyPageFailed(index, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish(int finished, int downloaded, int total) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetImageSuccess(int index, Image image) {
|
||||
notifyPageSucceed(index, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetImageFailure(int index, String error) {
|
||||
notifyPageFailed(index, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
|
||||
if (mSpiderQueen != null) {
|
||||
mSpiderQueen.preloadPages(pages, pair);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReleaseTask implements Runnable {
|
||||
|
||||
private SpiderQueen mSpiderQueen;
|
||||
|
||||
public ReleaseTask(SpiderQueen spiderQueen) {
|
||||
mSpiderQueen = spiderQueen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (null != mSpiderQueen) {
|
||||
SpiderQueen.releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ);
|
||||
mSpiderQueen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.yorozuya.SimpleHandler
|
||||
import java.util.Locale
|
||||
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener {
|
||||
private lateinit var mSpiderQueen: SpiderQueen
|
||||
override fun start() {
|
||||
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
||||
mSpiderQueen.addOnSpiderListener(this)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
mSpiderQueen.removeOnSpiderListener(this)
|
||||
// Activity recreate may called, so wait 3000s
|
||||
SimpleHandler.getInstance().postDelayed(ReleaseTask(mSpiderQueen), 3000)
|
||||
}
|
||||
|
||||
override val startPage
|
||||
get() = mSpiderQueen.startPage
|
||||
|
||||
override fun getImageFilename(index: Int): String {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
|
||||
override fun getImageFilenameWithExtension(index: Int): String {
|
||||
val extension = mSpiderQueen.getExtension(index)
|
||||
if (extension != null) {
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d.%s",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1,
|
||||
extension
|
||||
)
|
||||
}
|
||||
return String.format(
|
||||
Locale.US,
|
||||
"%d-%s-%08d",
|
||||
mGalleryInfo.gid,
|
||||
mGalleryInfo.token,
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
return mSpiderQueen.save(index, file)
|
||||
}
|
||||
|
||||
override fun save(index: Int, dir: UniFile, filename: String): UniFile? {
|
||||
return mSpiderQueen.save(index, dir, filename)
|
||||
}
|
||||
|
||||
override fun putStartPage(page: Int) {
|
||||
mSpiderQueen.putStartPage(page)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = mSpiderQueen.size
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
mSpiderQueen.request(index)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int) {
|
||||
mSpiderQueen.forceRequest(index)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
return mSpiderQueen.awaitReady()
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = mSpiderQueen.isReady
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mSpiderQueen.cancelRequest(index)
|
||||
}
|
||||
|
||||
override fun onGetPages(pages: Int) {}
|
||||
|
||||
override fun onGet509(index: Int) {}
|
||||
|
||||
override fun onPageDownload(
|
||||
index: Int,
|
||||
contentLength: Long,
|
||||
receivedSize: Long,
|
||||
bytesRead: Int
|
||||
) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onPageFailure(
|
||||
index: Int,
|
||||
error: String?,
|
||||
finished: Int,
|
||||
downloaded: Int,
|
||||
total: Int
|
||||
) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onGetImageSuccess(index: Int, image: Image?) {
|
||||
notifyPageSucceed(index, image!!)
|
||||
}
|
||||
|
||||
override fun onGetImageFailure(index: Int, error: String?) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, pair)
|
||||
}
|
||||
|
||||
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
|
||||
override fun run() {
|
||||
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
|
||||
mSpiderQueen = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.unifile.UniFile;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
|
||||
public abstract class PageLoader2 extends PageLoader {
|
||||
|
||||
// With dot
|
||||
public static final String[] SUPPORT_IMAGE_EXTENSIONS = {
|
||||
".jpg", // Joint Photographic Experts Group
|
||||
".png", // Portable Network Graphics
|
||||
".gif", // Graphics Interchange Format
|
||||
};
|
||||
|
||||
public int getStartPage() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void putStartPage(int page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return without extension
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getImageFilename(int index);
|
||||
|
||||
/**
|
||||
* @return with extension
|
||||
*/
|
||||
@NonNull
|
||||
public abstract String getImageFilenameWithExtension(int index);
|
||||
|
||||
public abstract boolean save(int index, @NonNull UniFile file);
|
||||
|
||||
/**
|
||||
* @param filename without extension
|
||||
*/
|
||||
@Nullable
|
||||
public abstract UniFile save(int index, @NonNull UniFile dir, @NonNull String filename);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
|
||||
abstract class PageLoader2 : PageLoader() {
|
||||
open val startPage: Int
|
||||
get() = 0
|
||||
|
||||
open fun putStartPage(page: Int) {}
|
||||
|
||||
/**
|
||||
* @return without extension
|
||||
*/
|
||||
abstract fun getImageFilename(index: Int): String
|
||||
|
||||
/**
|
||||
* @return with extension
|
||||
*/
|
||||
abstract fun getImageFilenameWithExtension(index: Int): String
|
||||
abstract fun save(index: Int, file: UniFile): Boolean
|
||||
|
||||
/**
|
||||
* @param filename without extension
|
||||
*/
|
||||
abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?
|
||||
}
|
||||
|
||||
val SUPPORT_IMAGE_EXTENSIONS = arrayOf(
|
||||
".jpg", // Joint Photographic Experts Group
|
||||
".png", // Portable Network Graphics
|
||||
".gif"
|
||||
)
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.download.DownloadManager;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
public class CleanRedundancyPreference extends TaskPreference {
|
||||
|
||||
public CleanRedundancyPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CleanRedundancyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Task onCreateTask() {
|
||||
return new ClearTask(getContext());
|
||||
}
|
||||
|
||||
private static class ClearTask extends Task {
|
||||
|
||||
private final DownloadManager mManager;
|
||||
|
||||
public ClearTask(@NonNull Context context) {
|
||||
super(context);
|
||||
mManager = EhApplication.getDownloadManager();
|
||||
}
|
||||
|
||||
// True for cleared
|
||||
private boolean clearFile(UniFile file) {
|
||||
String name = file.getName();
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
int index = name.indexOf('-');
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index);
|
||||
}
|
||||
long gid = NumberUtils.parseLongSafely(name, -1L);
|
||||
if (-1L == gid) {
|
||||
return false;
|
||||
}
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
return false;
|
||||
}
|
||||
file.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackground(Void... params) {
|
||||
UniFile dir = Settings.getDownloadLocation();
|
||||
if (null == dir) {
|
||||
return 0;
|
||||
}
|
||||
UniFile[] files = dir.listFiles();
|
||||
if (null == files) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (UniFile f : files) {
|
||||
if (clearFile(f)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
int count;
|
||||
if (o instanceof Integer) {
|
||||
count = (Integer) o;
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
showTip(0 == count ?
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_no_redundancy) :
|
||||
mApplication.getString(R.string.settings_download_clean_redundancy_done, count), Snackbar.LENGTH_SHORT);
|
||||
super.onPostExecute(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val NO_REDUNDANCY =
|
||||
GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)
|
||||
private val CLEAR_REDUNDANCY_DONE =
|
||||
{ cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }
|
||||
private val FINAL_CLEAR_REDUNDANCY_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }
|
||||
|
||||
class CleanRedundancyPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
private fun clearFile(file: UniFile): Boolean {
|
||||
var name = file.name ?: return false
|
||||
val index = name.indexOf('-')
|
||||
if (index >= 0) {
|
||||
name = name.substring(0, index)
|
||||
}
|
||||
val gid = name.toLongOrNull() ?: return false
|
||||
if (downloadManager.containDownloadInfo(gid)) {
|
||||
return false
|
||||
}
|
||||
file.delete()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun doRealWork(): Int {
|
||||
return Settings.getDownloadLocation()?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0
|
||||
}
|
||||
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
else singletonJob = launch {
|
||||
val cnt = doRealWork()
|
||||
withUIContext {
|
||||
showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -15,145 +15,111 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.IOException
|
||||
import java.util.Collections
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
class RestoreDownloadPreference constructor(
|
||||
context: Context, attrs: AttributeSet?
|
||||
private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)
|
||||
private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)
|
||||
private val RESTORE_COUNT_MSG =
|
||||
{ cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }
|
||||
|
||||
class RestoreDownloadPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : TaskPreference(context, attrs) {
|
||||
override fun onCreateTask(): Task {
|
||||
return RestoreTask(context)
|
||||
}
|
||||
|
||||
private class RestoreTask(context: Context) : Task(context) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) {
|
||||
private val mManager: DownloadManager = downloadManager
|
||||
private var restoreDirCount = 0
|
||||
private fun getRestoreItem(file: UniFile?): RestoreItem? {
|
||||
if (null == file || !file.isDirectory) return null
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
return runCatching {
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
var gid = spiderInfo.gid
|
||||
var dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
return null
|
||||
}
|
||||
val siFile = file.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return null
|
||||
return try {
|
||||
val spiderInfo = readCompatFromUniFile(siFile) ?: return null
|
||||
val gid = spiderInfo.gid
|
||||
val dirname = file.name
|
||||
if (mManager.containDownloadInfo(gid)) {
|
||||
// Restore download dir to avoid redownload
|
||||
val dbdirname = EhDB.getDownloadDirname(gid)
|
||||
if (null == dbdirname || dirname != dbdirname) {
|
||||
EhDB.putDownloadDirname(gid, dirname)
|
||||
restoreDirCount++
|
||||
}
|
||||
return null
|
||||
}
|
||||
val token = spiderInfo.token
|
||||
val restoreItem = RestoreItem()
|
||||
restoreItem.gid = gid
|
||||
restoreItem.token = token
|
||||
restoreItem.dirname = dirname
|
||||
restoreItem
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
RestoreItem().apply {
|
||||
gid = spiderInfo.gid
|
||||
token = spiderInfo.token
|
||||
dirname = file.name
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params: Void): Any? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val restoreItemList: MutableList<RestoreItem> = ArrayList()
|
||||
val files = dir.listFiles() ?: return null
|
||||
for (file in files) {
|
||||
val restoreItem = getRestoreItem(file)
|
||||
if (null != restoreItem) {
|
||||
restoreItemList.add(restoreItem)
|
||||
}
|
||||
}
|
||||
return if (0 == restoreItemList.size) {
|
||||
Collections.EMPTY_LIST
|
||||
} else try {
|
||||
runBlocking {
|
||||
fillGalleryListByApi(
|
||||
ArrayList(restoreItemList),
|
||||
EhUrl.referer
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostExecute(o: Any) {
|
||||
if (o !is List<*>) {
|
||||
mActivity.showTip(R.string.settings_download_restore_failed, BaseScene.LENGTH_SHORT)
|
||||
} else {
|
||||
val list = o as List<RestoreItem>
|
||||
if (list.isEmpty()) {
|
||||
mActivity.showTip(
|
||||
if (restoreDirCount == 0)
|
||||
mActivity.getString(R.string.settings_download_restore_not_found)
|
||||
else
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = list.size
|
||||
while (i < n) {
|
||||
val item = list[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, item.dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(
|
||||
mActivity.getString(
|
||||
R.string.settings_download_restore_successfully,
|
||||
count + restoreDirCount
|
||||
),
|
||||
BaseScene.LENGTH_SHORT
|
||||
)
|
||||
val preference: Preference? = preference
|
||||
if (null != preference) {
|
||||
val context = preference.context
|
||||
if (context is Activity) {
|
||||
context.setResult(Activity.RESULT_OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onPostExecute(o)
|
||||
}
|
||||
private suspend fun doRealWork(): List<GalleryInfo>? {
|
||||
val dir = Settings.getDownloadLocation() ?: return null
|
||||
val files = dir.listFiles() ?: return null
|
||||
val restoreItemList = files.mapNotNull { getRestoreItem(it) }
|
||||
return runSuspendCatching {
|
||||
fillGalleryListByApi(restoreItemList, EhUrl.referer)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private class RestoreItem : BaseGalleryInfo() {
|
||||
var dirname: String? = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun launchJob() {
|
||||
if (singletonJob?.isActive == true) singletonJob?.invokeOnCompletion {
|
||||
launchUI {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
else singletonJob = launch {
|
||||
val result = doRealWork()
|
||||
withUIContext {
|
||||
if (result == null) {
|
||||
showTip(RESTORE_FAILED)
|
||||
} else {
|
||||
if (result.isEmpty()) {
|
||||
showTip(RESTORE_COUNT_MSG(restoreDirCount))
|
||||
} else {
|
||||
var count = 0
|
||||
var i = 0
|
||||
val n = result.size
|
||||
while (i < n) {
|
||||
val item = result[i]
|
||||
// Avoid failed gallery info
|
||||
if (null != item.title) {
|
||||
// Put to download
|
||||
mManager.addDownload(item, null)
|
||||
// Put download dir to DB
|
||||
EhDB.putDownloadDirname(item.gid, (item as RestoreItem).dirname)
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
showTip(RESTORE_COUNT_MSG(count + restoreDirCount))
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var singletonJob: Job? = null
|
||||
@@ -1,208 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.customview.view.AbsSavedState;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.hippo.preference.DialogPreference;
|
||||
import com.hippo.util.IoThreadPoolExecutor;
|
||||
import com.hippo.yorozuya.IntIdGenerator;
|
||||
|
||||
public abstract class TaskPreference extends DialogPreference {
|
||||
|
||||
private Task mTask;
|
||||
private int mTaskId = IntIdGenerator.INVALID_ID;
|
||||
|
||||
public TaskPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TaskPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
builder.setTitle(null);
|
||||
builder.setView(R.layout.preference_dialog_task);
|
||||
builder.setCancelable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogCreated(AlertDialog dialog) {
|
||||
if (null == mTask) {
|
||||
mTask = onCreateTask();
|
||||
mTask.setPreference(this);
|
||||
mTaskId = ((EhApplication) getContext().getApplicationContext()).putGlobalStuff(mTask);
|
||||
mTask.executeOnExecutor(IoThreadPoolExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTaskEnd() {
|
||||
// Dismiss dialog
|
||||
Dialog dialog = getDialog();
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// Clear async
|
||||
mTask = null;
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract Task onCreateTask();
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState myState = new SavedState(superState);
|
||||
myState.asyncTaskId = mTaskId;
|
||||
return myState;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state == null || !state.getClass().equals(SavedState.class)) {
|
||||
// Didn't save state for us in onSaveInstanceState
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
SavedState myState = (SavedState) state;
|
||||
mTaskId = myState.asyncTaskId;
|
||||
if (IntIdGenerator.INVALID_ID != mTaskId) {
|
||||
Object o = ((EhApplication) getContext().getApplicationContext()).getGlobalStuff(mTaskId);
|
||||
if (o instanceof Task) {
|
||||
mTask = (Task) o;
|
||||
mTask.setPreference(this);
|
||||
}
|
||||
}
|
||||
if (null == mTask) {
|
||||
mTaskId = IntIdGenerator.INVALID_ID;
|
||||
}
|
||||
|
||||
// TODO if not task, show not reopen dialog
|
||||
|
||||
super.onRestoreInstanceState(myState.getSuperState());
|
||||
}
|
||||
|
||||
private static class SavedState extends AbsSavedState {
|
||||
public static final Parcelable.Creator<SavedState> CREATOR =
|
||||
new Parcelable.Creator<SavedState>() {
|
||||
@Override
|
||||
public SavedState createFromParcel(Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
int asyncTaskId;
|
||||
|
||||
public SavedState(Parcel source) {
|
||||
super(source, SavedState.class.getClassLoader());
|
||||
asyncTaskId = source.readInt();
|
||||
}
|
||||
|
||||
public SavedState(Parcelable superState) {
|
||||
super(superState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeInt(asyncTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class Task extends AsyncTask<Void, Void, Object> {
|
||||
|
||||
protected final EhApplication mApplication;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
protected final SettingsActivity mActivity;
|
||||
@Nullable
|
||||
private TaskPreference mPreference;
|
||||
|
||||
public Task(@NonNull Context context) {
|
||||
mActivity = (SettingsActivity) context;
|
||||
mApplication = (EhApplication) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public EhApplication getApplication() {
|
||||
return mApplication;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TaskPreference getPreference() {
|
||||
return mPreference;
|
||||
}
|
||||
|
||||
public void setPreference(@Nullable TaskPreference preference) {
|
||||
mPreference = preference;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
mApplication.removeGlobalStuff(this);
|
||||
if (null != mPreference) {
|
||||
mPreference.onTaskEnd();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(@StringRes int id, int length) {
|
||||
try {
|
||||
mActivity.showTip(id, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, id, length).show();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showTip(CharSequence message, int length) {
|
||||
try {
|
||||
mActivity.showTip(message, length);
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(mApplication, message, length).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.ui.SettingsActivity
|
||||
import com.hippo.preference.DialogPreference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class TaskPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : DialogPreference(context, attrs), CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
protected lateinit var dialog: AlertDialog
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
builder.setTitle(null)
|
||||
builder.setView(R.layout.preference_dialog_task)
|
||||
builder.setCancelable(false)
|
||||
}
|
||||
|
||||
abstract fun launchJob()
|
||||
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
this.dialog = dialog
|
||||
launchJob()
|
||||
}
|
||||
|
||||
protected fun showTip(msg: String) {
|
||||
(context as SettingsActivity).showTip(
|
||||
msg,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@
|
||||
package com.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class VersionPreference @JvmOverloads constructor(
|
||||
@@ -26,13 +26,7 @@ class VersionPreference @JvmOverloads constructor(
|
||||
) : Preference(context, attrs) {
|
||||
init {
|
||||
setTitle(R.string.settings_about_version)
|
||||
@Suppress("DEPRECATION") val version: String = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
context.getString(R.string.error_unknown)
|
||||
}
|
||||
summary = version
|
||||
summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\n" +
|
||||
context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,13 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import android.graphics.ImageDecoder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_WRITE
|
||||
import coil.decode.DecodeUtils
|
||||
import coil.decode.FrameDelayRewritingSource
|
||||
import coil.decode.isGif
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.copyTo
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.EhDB
|
||||
@@ -28,7 +31,9 @@ import com.hippo.ehviewer.client.EhCacheKeyFactory
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhUtils.getSuitableTitle
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.coil.edit
|
||||
import com.hippo.ehviewer.coil.read
|
||||
import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS
|
||||
import com.hippo.image.Image.CloseableSource
|
||||
import com.hippo.unifile.RawFile
|
||||
import com.hippo.unifile.UniFile
|
||||
@@ -36,6 +41,7 @@ import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okhttp3.executeAsync
|
||||
import okio.buffer
|
||||
import java.io.File
|
||||
@@ -100,30 +106,27 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
* @param extension with dot
|
||||
*/
|
||||
private fun fixExtension(extension: String): String {
|
||||
return extension.takeIf { PageLoader2.SUPPORT_IMAGE_EXTENSIONS.contains(it) }
|
||||
?: PageLoader2.SUPPORT_IMAGE_EXTENSIONS[0]
|
||||
return extension.takeIf { SUPPORT_IMAGE_EXTENSIONS.contains(it) }
|
||||
?: SUPPORT_IMAGE_EXTENSIONS[0]
|
||||
}
|
||||
|
||||
private fun copyFromCacheToDownloadDir(index: Int): Boolean {
|
||||
val dir = downloadDir ?: return false
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
runCatching {
|
||||
(sCache[key] ?: return false).use { snapshot ->
|
||||
val extension = fixExtension("." + snapshot.metadata.toFile().readText())
|
||||
return runCatching {
|
||||
sCache.read(key) {
|
||||
val extension = fixExtension("." + metadata.toFile().readText())
|
||||
val file = dir.createFile(generateImageFilename(index, extension)) ?: return false
|
||||
(file.openOutputStream() as FileOutputStream).use { outputStream ->
|
||||
outputStream.channel.use { outChannel ->
|
||||
snapshot.data.toFile().inputStream().use { inputStream ->
|
||||
inputStream.channel.use {
|
||||
outChannel.transferFrom(it, 0, it.size())
|
||||
}
|
||||
}
|
||||
ParcelFileDescriptor.open(data.toFile(), MODE_READ_WRITE).use {
|
||||
it copyTo outputStream.fd
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
operator fun contains(index: Int): Boolean {
|
||||
@@ -194,7 +197,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
return receivedSize
|
||||
}
|
||||
|
||||
findDownloadFileForIndex(index, extension)?.runCatching {
|
||||
findDownloadFileForIndex(index, extension)?.runSuspendCatching {
|
||||
openOutputStream().use { outputStream ->
|
||||
(outputStream as FileOutputStream).channel.use { return doSave(it) == length }
|
||||
}
|
||||
@@ -206,19 +209,17 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
// Read Mode, allow save to cache
|
||||
if (mMode == SpiderQueen.MODE_READ) {
|
||||
val key = EhCacheKeyFactory.getImageKey(mGid, index)
|
||||
val editor = sCache.edit(key) ?: return false
|
||||
var received: Long = 0
|
||||
runCatching {
|
||||
editor.metadata.toFile().writeText(extension)
|
||||
editor.data.toFile().outputStream().use { outputStream ->
|
||||
outputStream.channel.use { received = doSave(it) }
|
||||
runSuspendCatching {
|
||||
sCache.edit(key) {
|
||||
metadata.toFile().writeText(extension)
|
||||
data.toFile().outputStream().use { outputStream ->
|
||||
outputStream.channel.use { received = doSave(it) }
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
editor.abort()
|
||||
it.printStackTrace()
|
||||
return false
|
||||
}.onSuccess {
|
||||
editor.commit()
|
||||
return received == length
|
||||
}
|
||||
}
|
||||
@@ -333,6 +334,8 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
).build()
|
||||
}
|
||||
|
||||
private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + ".jpeg"
|
||||
|
||||
fun getGalleryDownloadDir(gid: Long): UniFile? {
|
||||
val dir = Settings.getDownloadLocation()
|
||||
// Read from DB
|
||||
@@ -355,7 +358,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) {
|
||||
}
|
||||
|
||||
private fun findImageFile(dir: UniFile, index: Int): UniFile? {
|
||||
for (extension in PageLoader2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
for (extension in COMPAT_IMAGE_EXTENSIONS) {
|
||||
val filename = generateImageFilename(index, extension)
|
||||
val file = dir.findFile(filename)
|
||||
if (file != null) {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.compat.readLegacySpiderInfo
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.unifile.UniFile
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import java.io.File
|
||||
|
||||
@Serializable
|
||||
class SpiderInfo @JvmOverloads constructor(
|
||||
@@ -18,3 +26,50 @@ class SpiderInfo @JvmOverloads constructor(
|
||||
|
||||
var previewPerPage: Int = -1
|
||||
)
|
||||
|
||||
fun SpiderInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
it.write(Cbor.encodeToByteArray(this))
|
||||
}
|
||||
}
|
||||
|
||||
fun SpiderInfo.saveToCache() {
|
||||
val entry = spiderInfoCache.edit(gid.toString()) ?: return
|
||||
runCatching {
|
||||
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
entry.abort()
|
||||
}.onSuccess {
|
||||
entry.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private val spiderInfoCache by lazy {
|
||||
DiskCache.Builder()
|
||||
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
|
||||
.maxSizeBytes(20 * 1024 * 1024).build()
|
||||
}
|
||||
|
||||
fun readFromCache(gid: Long): SpiderInfo? {
|
||||
val snapshot = spiderInfoCache[gid.toString()] ?: return null
|
||||
return runCatching {
|
||||
snapshot.use {
|
||||
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
|
||||
return runCatching {
|
||||
file.openInputStream().use {
|
||||
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
||||
}
|
||||
}.getOrNull() ?: runCatching {
|
||||
file.openInputStream().use { readLegacySpiderInfo(it) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
const val TOKEN_FAILED = "failed"
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import coil.disk.DiskCache
|
||||
import com.hippo.compat.readLegacySpiderInfo
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.unifile.UniFile
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import java.io.File
|
||||
|
||||
fun SpiderInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
it.write(Cbor.encodeToByteArray(this))
|
||||
}
|
||||
}
|
||||
|
||||
fun SpiderInfo.saveToCache() {
|
||||
val entry = spiderInfoCache.edit(gid.toString()) ?: return
|
||||
runCatching {
|
||||
entry.data.toFile().writeBytes(Cbor.encodeToByteArray(this))
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
entry.abort()
|
||||
}.onSuccess {
|
||||
entry.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private val spiderInfoCache by lazy {
|
||||
DiskCache.Builder()
|
||||
.directory(File(EhApplication.application.cacheDir, "spider_info_v2_1"))
|
||||
.maxSizeBytes(20 * 1024 * 1024).build()
|
||||
}
|
||||
|
||||
fun readFromCache(gid: Long): SpiderInfo? {
|
||||
val snapshot = spiderInfoCache[gid.toString()] ?: return null
|
||||
return runCatching {
|
||||
snapshot.use {
|
||||
return Cbor.decodeFromByteArray(it.data.toFile().readBytes())
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun readCompatFromUniFile(file: UniFile): SpiderInfo? {
|
||||
return runCatching {
|
||||
file.openInputStream().use {
|
||||
Cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
|
||||
}
|
||||
}.getOrNull() ?: runCatching {
|
||||
file.openInputStream().use { readLegacySpiderInfo(it) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
const val TOKEN_FAILED = "failed"
|
||||
@@ -1,748 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.spider;
|
||||
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.collection.LongSparseArray;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder;
|
||||
import com.hippo.ehviewer.client.EhUrl;
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo;
|
||||
import com.hippo.ehviewer.client.data.PreviewSet;
|
||||
import com.hippo.ehviewer.client.exception.ParseException;
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser;
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader;
|
||||
import kotlin.Pair;
|
||||
import kotlinx.coroutines.CoroutineScopeKt;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public final class SpiderQueen implements Runnable {
|
||||
|
||||
public static final int MODE_READ = 0;
|
||||
public static final int MODE_DOWNLOAD = 1;
|
||||
public static final int STATE_NONE = 0;
|
||||
public static final int STATE_DOWNLOADING = 1;
|
||||
public static final int STATE_FINISHED = 2;
|
||||
public static final int STATE_FAILED = 3;
|
||||
public static final String SPIDER_INFO_FILENAME = ".ehviewer";
|
||||
private static final String TAG = SpiderQueen.class.getSimpleName();
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
private static final boolean DEBUG_LOG = true;
|
||||
private static final boolean DEBUG_PTOKEN = true;
|
||||
private static final LongSparseArray<SpiderQueen> sQueenMap = new LongSparseArray<>();
|
||||
public static String PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error);
|
||||
public static String ERROR_509 = GetText.getString(R.string.error_509);
|
||||
public static String NETWORK_ERROR = GetText.getString(R.string.error_socket);
|
||||
|
||||
public static String DECODE_ERROR = GetText.getString(R.string.error_decoding_failed);
|
||||
@NonNull
|
||||
public final SpiderDen mSpiderDen;
|
||||
public final AtomicReference<SpiderInfo> mSpiderInfo = new AtomicReference<>();
|
||||
// Store page download percent
|
||||
public final ConcurrentHashMap<Integer, Float> mPagePercentMap = new ConcurrentHashMap<>();
|
||||
@NonNull
|
||||
private final OkHttpClient mHttpClient;
|
||||
@NonNull
|
||||
private final GalleryInfo mGalleryInfo;
|
||||
private final Object mQueenLock = new Object();
|
||||
private final Object mPageStateLock = new Object();
|
||||
private final AtomicInteger mDownloadedPages = new AtomicInteger(0);
|
||||
private final AtomicInteger mFinishedPages = new AtomicInteger(0);
|
||||
// Store page error
|
||||
private final ConcurrentHashMap<Integer, String> mPageErrorMap = new ConcurrentHashMap<>();
|
||||
private final List<OnSpiderListener> mSpiderListeners = new ArrayList<>();
|
||||
public volatile int[] mPageStateArray;
|
||||
private int mReadReference = 0;
|
||||
private int mDownloadReference = 0;
|
||||
// It mQueenThread is null, failed or stopped
|
||||
@Nullable
|
||||
private volatile Thread mQueenThread;
|
||||
private SpiderQueenWorker mWorkerScope;
|
||||
private boolean mStoped = false;
|
||||
|
||||
private SpiderQueen(@NonNull GalleryInfo galleryInfo) {
|
||||
mHttpClient = EhApplication.getOkHttpClient();
|
||||
mGalleryInfo = galleryInfo;
|
||||
mSpiderDen = new SpiderDen(mGalleryInfo);
|
||||
mWorkerScope = new SpiderQueenWorker(this);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public static SpiderQueen obtainSpiderQueen(@NonNull GalleryInfo galleryInfo, @Mode int mode) {
|
||||
OSUtils.checkMainLoop();
|
||||
|
||||
SpiderQueen queen = sQueenMap.get(galleryInfo.getGid());
|
||||
if (queen == null) {
|
||||
queen = new SpiderQueen(galleryInfo);
|
||||
sQueenMap.put(galleryInfo.getGid(), queen);
|
||||
// Set mode
|
||||
queen.setMode(mode);
|
||||
queen.start();
|
||||
} else {
|
||||
// Set mode
|
||||
queen.setMode(mode);
|
||||
}
|
||||
return queen;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public static void releaseSpiderQueen(@NonNull SpiderQueen queen, @Mode int mode) {
|
||||
OSUtils.checkMainLoop();
|
||||
|
||||
// Clear mode
|
||||
queen.clearMode(mode);
|
||||
|
||||
if (queen.mReadReference == 0 && queen.mDownloadReference == 0) {
|
||||
// Stop and remove if there is no reference
|
||||
queen.stop();
|
||||
sQueenMap.remove(queen.mGalleryInfo.getGid());
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean contain(int[] array, int value) {
|
||||
for (int v : array) {
|
||||
if (v == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as suspend fun when kotlinize, need IO
|
||||
public static int getStartPage(long gid) {
|
||||
var queen = sQueenMap.get(gid);
|
||||
SpiderInfo spiderInfo = null;
|
||||
|
||||
// Fast Path: read existing queen
|
||||
if (queen != null) {
|
||||
spiderInfo = queen.mSpiderInfo.get();
|
||||
}
|
||||
|
||||
// Slow path, read diskcache
|
||||
if (spiderInfo == null) {
|
||||
spiderInfo = SpiderInfoUtilsKt.readFromCache(gid);
|
||||
}
|
||||
|
||||
if (spiderInfo == null) return 0;
|
||||
return spiderInfo.getStartPage();
|
||||
}
|
||||
|
||||
public void addOnSpiderListener(OnSpiderListener listener) {
|
||||
synchronized (mSpiderListeners) {
|
||||
mSpiderListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeOnSpiderListener(OnSpiderListener listener) {
|
||||
synchronized (mSpiderListeners) {
|
||||
mSpiderListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyGetPages(int pages) {
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onGetPages(pages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyGet509(int index) {
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onGet509(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyPageDownload(int index, long contentLength, long receivedSize, int bytesRead) {
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onPageDownload(index, contentLength, receivedSize, bytesRead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyPageSuccess(int index) {
|
||||
int size = -1;
|
||||
int[] temp = mPageStateArray;
|
||||
if (temp != null) {
|
||||
size = temp.length;
|
||||
}
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onPageSuccess(index, mFinishedPages.get(), mDownloadedPages.get(), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyPageFailure(int index, String error) {
|
||||
int size = -1;
|
||||
int[] temp = mPageStateArray;
|
||||
if (temp != null) {
|
||||
size = temp.length;
|
||||
}
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onPageFailure(index, error, mFinishedPages.get(), mDownloadedPages.get(), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyAllPageDownloaded() {
|
||||
int size = -1;
|
||||
int[] temp = mPageStateArray;
|
||||
if (temp != null) {
|
||||
size = temp.length;
|
||||
}
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onFinish(mFinishedPages.get(), mDownloadedPages.get(), size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyGetImageSuccess(int index, Image image) {
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onGetImageSuccess(index, image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyGetImageFailure(int index, String error) {
|
||||
if (error == null) {
|
||||
error = GetText.getString(R.string.error_unknown);
|
||||
}
|
||||
synchronized (mSpiderListeners) {
|
||||
for (OnSpiderListener listener : mSpiderListeners) {
|
||||
listener.onGetImageFailure(index, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mInDownloadMode = false;
|
||||
|
||||
private void updateMode() {
|
||||
int mode;
|
||||
if (mDownloadReference > 0) {
|
||||
mode = MODE_DOWNLOAD;
|
||||
} else {
|
||||
mode = MODE_READ;
|
||||
}
|
||||
|
||||
mSpiderDen.setMode(mode);
|
||||
|
||||
// Update download page
|
||||
boolean intoDownloadMode = mode == MODE_DOWNLOAD;
|
||||
|
||||
if (intoDownloadMode && !mInDownloadMode && mPageStateArray != null) {
|
||||
// Clear download state
|
||||
synchronized (mPageStateLock) {
|
||||
int[] temp = mPageStateArray;
|
||||
for (int i = 0, n = temp.length; i < n; i++) {
|
||||
int oldState = temp[i];
|
||||
if (STATE_DOWNLOADING != oldState) {
|
||||
temp[i] = STATE_NONE;
|
||||
}
|
||||
}
|
||||
mDownloadedPages.lazySet(0);
|
||||
mFinishedPages.lazySet(0);
|
||||
mPageErrorMap.clear();
|
||||
mPagePercentMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
mInDownloadMode = intoDownloadMode;
|
||||
}
|
||||
|
||||
private void setMode(@Mode int mode) {
|
||||
switch (mode) {
|
||||
case MODE_READ -> mReadReference++;
|
||||
case MODE_DOWNLOAD -> mDownloadReference++;
|
||||
}
|
||||
|
||||
if (mDownloadReference > 1) {
|
||||
throw new IllegalStateException("mDownloadReference can't more than 0");
|
||||
}
|
||||
|
||||
updateMode();
|
||||
}
|
||||
|
||||
private void clearMode(@Mode int mode) {
|
||||
switch (mode) {
|
||||
case MODE_READ -> mReadReference--;
|
||||
case MODE_DOWNLOAD -> mDownloadReference--;
|
||||
}
|
||||
|
||||
if (mReadReference < 0 || mDownloadReference < 0) {
|
||||
throw new IllegalStateException("Mode reference < 0");
|
||||
}
|
||||
|
||||
updateMode();
|
||||
}
|
||||
|
||||
private void start() {
|
||||
Thread queenThread = new PriorityThread(this, TAG + '-' + sIdGenerator.incrementAndGet(),
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
mQueenThread = queenThread;
|
||||
queenThread.start();
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
mStoped = true;
|
||||
synchronized (mQueenLock) {
|
||||
mQueenLock.notifyAll();
|
||||
}
|
||||
CoroutineScopeKt.cancel(mWorkerScope, null);
|
||||
mWorkerScope = null;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
if (mQueenThread == null) {
|
||||
return PageLoader.STATE_ERROR;
|
||||
} else if (mPageStateArray == null) {
|
||||
return PageLoader.STATE_WAIT;
|
||||
} else {
|
||||
return mPageStateArray.length;
|
||||
}
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
if (mQueenThread == null) {
|
||||
return "Error";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void forceRequest(int index) {
|
||||
request(index, true);
|
||||
}
|
||||
|
||||
public void request(int index) {
|
||||
request(index, false);
|
||||
}
|
||||
|
||||
private int getPageState(int index) {
|
||||
synchronized (mPageStateLock) {
|
||||
if (mPageStateArray != null && index >= 0 && index < mPageStateArray.length) {
|
||||
return mPageStateArray[index];
|
||||
} else {
|
||||
return STATE_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void cancelRequest(int index) {
|
||||
if (mQueenThread == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWorkerScope.cancelDecode(index);
|
||||
}
|
||||
|
||||
public void preloadPages(@NonNull List<Integer> pages, @NonNull Pair<Integer, Integer> pair) {
|
||||
if (mQueenThread == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWorkerScope.updateRAList(pages, pair);
|
||||
}
|
||||
|
||||
private void request(int index, boolean force) {
|
||||
if (mQueenThread == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get page state
|
||||
int state = getPageState(index);
|
||||
|
||||
// Fix state for force
|
||||
if ((force && (state == STATE_FINISHED || state == STATE_FAILED)) || (state == STATE_FAILED)) {
|
||||
// Update state to none at once
|
||||
updatePageState(index, STATE_NONE);
|
||||
}
|
||||
|
||||
mWorkerScope.launch(index, force);
|
||||
}
|
||||
|
||||
public boolean save(int index, @NonNull UniFile file) {
|
||||
int state = getPageState(index);
|
||||
if (STATE_FINISHED != state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mSpiderDen.saveToUniFile(index, file);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UniFile save(int index, @NonNull UniFile dir, @NonNull String filename) {
|
||||
int state = getPageState(index);
|
||||
if (STATE_FINISHED != state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var ext = mSpiderDen.getExtension(index);
|
||||
UniFile dst = dir.subFile(null != ext ? filename + "." + ext : filename);
|
||||
if (null == dst) {
|
||||
return null;
|
||||
}
|
||||
if (!mSpiderDen.saveToUniFile(index, dst)) return null;
|
||||
return dst;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getExtension(int index) {
|
||||
int state = getPageState(index);
|
||||
if (STATE_FINISHED != state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mSpiderDen.getExtension(index);
|
||||
}
|
||||
|
||||
public int getStartPage() {
|
||||
SpiderInfo spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo == null) spiderInfo = readSpiderInfoFromLocal();
|
||||
if (spiderInfo == null) return 0;
|
||||
return spiderInfo.getStartPage();
|
||||
}
|
||||
|
||||
public void putStartPage(int page) {
|
||||
final SpiderInfo spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo != null) {
|
||||
spiderInfo.setStartPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized SpiderInfo readSpiderInfoFromLocal() {
|
||||
SpiderInfo spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo != null) {
|
||||
return spiderInfo;
|
||||
}
|
||||
|
||||
// Read from download dir
|
||||
UniFile downloadDir = mSpiderDen.getDownloadDir();
|
||||
if (downloadDir != null) {
|
||||
UniFile file = downloadDir.findFile(SPIDER_INFO_FILENAME);
|
||||
if (file != null) {
|
||||
spiderInfo = SpiderInfoUtilsKt.readCompatFromUniFile(file);
|
||||
if (spiderInfo != null && spiderInfo.getGid() == mGalleryInfo.getGid() &&
|
||||
spiderInfo.getToken().equals(mGalleryInfo.getToken())) {
|
||||
return spiderInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read from cache
|
||||
spiderInfo = SpiderInfoUtilsKt.readFromCache(mGalleryInfo.getGid());
|
||||
if (spiderInfo != null && spiderInfo.getGid() == mGalleryInfo.getGid() && spiderInfo.getToken().equals(mGalleryInfo.getToken())) {
|
||||
return spiderInfo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void readPreviews(String body, int index, SpiderInfo spiderInfo) throws ParseException {
|
||||
spiderInfo.setPreviewPages(GalleryDetailParser.parsePreviewPages(body));
|
||||
PreviewSet previewSet = GalleryDetailParser.parsePreviewSet(body);
|
||||
|
||||
if (previewSet.size() > 0) {
|
||||
if (index == 0) {
|
||||
spiderInfo.setPreviewPerPage(previewSet.size());
|
||||
} else {
|
||||
spiderInfo.setPreviewPerPage(previewSet.getPosition(0) / index);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0, n = previewSet.size(); i < n; i++) {
|
||||
GalleryPageUrlParser.Result result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i));
|
||||
if (result != null) {
|
||||
spiderInfo.getPTokenMap().put(result.page, result.pToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SpiderInfo readSpiderInfoFromInternet() {
|
||||
Request request = new EhRequestBuilder(EhUrl.getGalleryDetailUrl(
|
||||
mGalleryInfo.getGid(), mGalleryInfo.getToken(), 0, false), EhUrl.getReferer()).build();
|
||||
try (Response response = mHttpClient.newCall(request).execute()) {
|
||||
String body = response.body().string();
|
||||
|
||||
var pages = GalleryDetailParser.parsePages(body);
|
||||
SpiderInfo spiderInfo = new SpiderInfo(mGalleryInfo.getGid(), pages);
|
||||
spiderInfo.setToken(mGalleryInfo.getToken());
|
||||
readPreviews(body, 0, spiderInfo);
|
||||
return spiderInfo;
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPTokenFromMultiPageViewer(int index) {
|
||||
SpiderInfo spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = EhUrl.getGalleryMultiPageViewerUrl(
|
||||
mGalleryInfo.getGid(), mGalleryInfo.getToken());
|
||||
if (DEBUG_PTOKEN) {
|
||||
Log.d(TAG, "getPTokenFromMultiPageViewer index " + index + ", url " + url);
|
||||
}
|
||||
String referer = EhUrl.getReferer();
|
||||
Request request = new EhRequestBuilder(url, referer).build();
|
||||
try (Response response = mHttpClient.newCall(request).execute()) {
|
||||
String body = response.body().string();
|
||||
|
||||
ArrayList<String> list = GalleryMultiPageViewerPTokenParser.parse(body);
|
||||
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
spiderInfo.getPTokenMap().put(i, list.get(i));
|
||||
}
|
||||
|
||||
return spiderInfo.getPTokenMap().get(index);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPTokenFromInternet(int index) {
|
||||
SpiderInfo spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check previewIndex
|
||||
int previewIndex;
|
||||
if (spiderInfo.getPreviewPerPage() >= 0) {
|
||||
previewIndex = index / spiderInfo.getPreviewPerPage();
|
||||
} else {
|
||||
previewIndex = 0;
|
||||
}
|
||||
if (spiderInfo.getPreviewPages() > 0) {
|
||||
previewIndex = Math.min(previewIndex, spiderInfo.getPreviewPages() - 1);
|
||||
}
|
||||
|
||||
String url = EhUrl.getGalleryDetailUrl(
|
||||
mGalleryInfo.getGid(), mGalleryInfo.getToken(), previewIndex, false);
|
||||
String referer = EhUrl.getReferer();
|
||||
if (DEBUG_PTOKEN) {
|
||||
Log.d(TAG, "index " + index + ", previewIndex " + previewIndex +
|
||||
", previewPerPage " + spiderInfo.getPreviewPerPage() + ", url " + url);
|
||||
}
|
||||
Request request = new EhRequestBuilder(url, referer).build();
|
||||
try (Response response = mHttpClient.newCall(request).execute()) {
|
||||
String body = response.body().string();
|
||||
readPreviews(body, previewIndex, spiderInfo);
|
||||
|
||||
return spiderInfo.getPTokenMap().get(index);
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void writeSpiderInfoToLocal() {
|
||||
var spiderInfo = mSpiderInfo.get();
|
||||
if (spiderInfo != null) {
|
||||
UniFile downloadDir = mSpiderDen.getDownloadDir();
|
||||
if (downloadDir != null) {
|
||||
UniFile file = downloadDir.createFile(SPIDER_INFO_FILENAME);
|
||||
SpiderInfoUtilsKt.write(spiderInfo, file);
|
||||
}
|
||||
SpiderInfoUtilsKt.saveToCache(spiderInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void runInternal() {
|
||||
// Read spider info
|
||||
SpiderInfo spiderInfo = readSpiderInfoFromLocal();
|
||||
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Spider info from internet
|
||||
if (spiderInfo == null) {
|
||||
spiderInfo = readSpiderInfoFromInternet();
|
||||
}
|
||||
|
||||
// Error! Can't get spiderInfo
|
||||
if (spiderInfo == null) {
|
||||
return;
|
||||
}
|
||||
mSpiderInfo.set(spiderInfo);
|
||||
|
||||
// Check Stopped
|
||||
if (mStoped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup page state
|
||||
synchronized (mPageStateLock) {
|
||||
mPageStateArray = new int[spiderInfo.getPages()];
|
||||
}
|
||||
|
||||
// Notify get pages
|
||||
notifyGetPages(spiderInfo.getPages());
|
||||
|
||||
if (mInDownloadMode) mWorkerScope.enterDownloadMode();
|
||||
|
||||
// Wait finish
|
||||
synchronized (mQueenLock) {
|
||||
try {
|
||||
mQueenLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
writeSpiderInfoToLocal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG_LOG) {
|
||||
Log.i(TAG, Thread.currentThread().getName() + ": start");
|
||||
}
|
||||
|
||||
runInternal();
|
||||
|
||||
// Set mQueenThread null
|
||||
mQueenThread = null;
|
||||
|
||||
if (DEBUG_LOG) {
|
||||
Log.i(TAG, Thread.currentThread().getName() + ": end");
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePageState(int index, @State int state) {
|
||||
updatePageState(index, state, null);
|
||||
}
|
||||
|
||||
private boolean isStateDone(int state) {
|
||||
return state == STATE_FINISHED || state == STATE_FAILED;
|
||||
}
|
||||
|
||||
public void updatePageState(int index, @State int state, String error) {
|
||||
int oldState;
|
||||
synchronized (mPageStateLock) {
|
||||
oldState = mPageStateArray[index];
|
||||
mPageStateArray[index] = state;
|
||||
|
||||
if (!isStateDone(oldState) && isStateDone(state)) {
|
||||
mDownloadedPages.incrementAndGet();
|
||||
} else if (isStateDone(oldState) && !isStateDone(state)) {
|
||||
mDownloadedPages.decrementAndGet();
|
||||
}
|
||||
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
|
||||
mFinishedPages.incrementAndGet();
|
||||
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
|
||||
mFinishedPages.decrementAndGet();
|
||||
}
|
||||
|
||||
// Clear
|
||||
if (state == STATE_DOWNLOADING) {
|
||||
mPageErrorMap.remove(index);
|
||||
} else if (state == STATE_FINISHED || state == STATE_FAILED) {
|
||||
mPagePercentMap.remove(index);
|
||||
}
|
||||
|
||||
// Get default error
|
||||
if (state == STATE_FAILED) {
|
||||
if (error == null) {
|
||||
error = GetText.getString(R.string.error_unknown);
|
||||
}
|
||||
mPageErrorMap.put(index, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
if (state == STATE_FAILED) {
|
||||
notifyPageFailure(index, error);
|
||||
} else if (state == STATE_FINISHED) {
|
||||
notifyPageSuccess(index);
|
||||
}
|
||||
|
||||
if (mFinishedPages.get() == size()) notifyAllPageDownloaded();
|
||||
}
|
||||
|
||||
@IntDef({MODE_READ, MODE_DOWNLOAD})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Mode {
|
||||
}
|
||||
|
||||
@IntDef({STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface State {
|
||||
}
|
||||
|
||||
public interface OnSpiderListener {
|
||||
|
||||
void onGetPages(int pages);
|
||||
|
||||
void onGet509(int index);
|
||||
|
||||
/**
|
||||
* @param contentLength -1 for unknown
|
||||
*/
|
||||
void onPageDownload(int index, long contentLength, long receivedSize, int bytesRead);
|
||||
|
||||
void onPageSuccess(int index, int finished, int downloaded, int total);
|
||||
|
||||
void onPageFailure(int index, String error, int finished, int downloaded, int total);
|
||||
|
||||
/**
|
||||
* All workers end
|
||||
*/
|
||||
void onFinish(int finished, int downloaded, int total);
|
||||
|
||||
void onGetImageSuccess(int index, Image image);
|
||||
|
||||
void onGetImageFailure(int index, String error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
* Rewrite with Kotlin coroutines, Tarsin Norbin 2023
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
||||
import com.hippo.ehviewer.GetText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhRequestBuilder
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.getGalleryDetailUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.getGalleryMultiPageViewerUrl
|
||||
import com.hippo.ehviewer.client.EhUrl.referer
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePages
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewPages
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser.parsePreviewSet
|
||||
import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageUrlParser
|
||||
import com.hippo.image.Image
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
|
||||
@Volatile
|
||||
lateinit var mPageStateArray: IntArray
|
||||
lateinit var mSpiderInfo: SpiderInfo
|
||||
|
||||
val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)
|
||||
val mPagePercentMap = ConcurrentHashMap<Int, Float>()
|
||||
private val mPageStateLock = Any()
|
||||
private val mDownloadedPages = AtomicInteger(0)
|
||||
private val mFinishedPages = AtomicInteger(0)
|
||||
private val mPageErrorMap = ConcurrentHashMap<Int, String>()
|
||||
private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()
|
||||
|
||||
private var mReadReference = 0
|
||||
private var mDownloadReference = 0
|
||||
|
||||
fun addOnSpiderListener(listener: OnSpiderListener) {
|
||||
synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }
|
||||
}
|
||||
|
||||
fun removeOnSpiderListener(listener: OnSpiderListener) {
|
||||
synchronized(mSpiderListeners) { mSpiderListeners.remove(listener) }
|
||||
}
|
||||
|
||||
private fun notifyGetPages(pages: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach { it.onGetPages(pages) }
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGet509(index: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach { it.onGet509(index) }
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageDownload(
|
||||
index,
|
||||
contentLength,
|
||||
receivedSize,
|
||||
bytesRead
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyPageSuccess(index: Int) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageSuccess(
|
||||
index,
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyPageFailure(index: Int, error: String?) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onPageFailure(
|
||||
index,
|
||||
error,
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyAllPageDownloaded() {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onFinish(
|
||||
mFinishedPages.get(),
|
||||
mDownloadedPages.get(),
|
||||
mPageStateArray.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGetImageSuccess(index: Int, image: Image) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyGetImageFailure(index: Int, error: String) {
|
||||
synchronized(mSpiderListeners) {
|
||||
mSpiderListeners.forEach {
|
||||
it.onGetImageFailure(index, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadMode = false
|
||||
val isReady
|
||||
get() = this::mSpiderInfo.isInitialized && this::mPageStateArray.isInitialized
|
||||
|
||||
@Synchronized
|
||||
private fun updateMode() {
|
||||
if (!isReady) return
|
||||
val mode: Int = if (mDownloadReference > 0) {
|
||||
MODE_DOWNLOAD
|
||||
} else {
|
||||
MODE_READ
|
||||
}
|
||||
mSpiderDen.setMode(mode)
|
||||
|
||||
// Update download page
|
||||
val intoDownloadMode = mode == MODE_DOWNLOAD
|
||||
if (intoDownloadMode && !downloadMode) {
|
||||
// Clear download state
|
||||
synchronized(mPageStateLock) {
|
||||
val temp: IntArray = mPageStateArray
|
||||
var i = 0
|
||||
val n = temp.size
|
||||
while (i < n) {
|
||||
val oldState = temp[i]
|
||||
if (STATE_DOWNLOADING != oldState) {
|
||||
temp[i] = STATE_NONE
|
||||
}
|
||||
i++
|
||||
}
|
||||
mDownloadedPages.lazySet(0)
|
||||
mFinishedPages.lazySet(0)
|
||||
mPageErrorMap.clear()
|
||||
mPagePercentMap.clear()
|
||||
}
|
||||
mWorkerScope.enterDownloadMode()
|
||||
}
|
||||
downloadMode = intoDownloadMode
|
||||
}
|
||||
|
||||
private fun setMode(@Mode mode: Int) {
|
||||
when (mode) {
|
||||
MODE_READ -> mReadReference++
|
||||
MODE_DOWNLOAD -> mDownloadReference++
|
||||
}
|
||||
check(mDownloadReference <= 1) { "mDownloadReference can't more than 0" }
|
||||
}
|
||||
|
||||
private fun clearMode(@Mode mode: Int) {
|
||||
when (mode) {
|
||||
MODE_READ -> mReadReference--
|
||||
MODE_DOWNLOAD -> mDownloadReference--
|
||||
}
|
||||
check(!(mReadReference < 0 || mDownloadReference < 0)) { "Mode reference < 0" }
|
||||
}
|
||||
|
||||
private val prepareJob = launchIO { doPrepare() }
|
||||
|
||||
private suspend fun doPrepare() {
|
||||
mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return
|
||||
mPageStateArray = IntArray(mSpiderInfo.pages)
|
||||
notifyGetPages(mSpiderInfo.pages)
|
||||
}
|
||||
|
||||
suspend fun awaitReady(): Boolean {
|
||||
prepareJob.join()
|
||||
return isReady
|
||||
}
|
||||
|
||||
suspend fun awaitStartPage(): Int {
|
||||
prepareJob.join()
|
||||
if (!isReady) return 0
|
||||
return mSpiderInfo.startPage
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
launchNonCancellable { runCatching { writeSpiderInfoToLocal() } }
|
||||
cancel()
|
||||
}
|
||||
|
||||
val size
|
||||
get() = mPageStateArray.size
|
||||
|
||||
val error: String?
|
||||
get() = null
|
||||
|
||||
fun forceRequest(index: Int) {
|
||||
request(index, true)
|
||||
}
|
||||
|
||||
fun request(index: Int) {
|
||||
request(index, false)
|
||||
}
|
||||
|
||||
private fun getPageState(index: Int): Int {
|
||||
synchronized(mPageStateLock) {
|
||||
return if (index >= 0 && index < mPageStateArray.size) {
|
||||
mPageStateArray[index]
|
||||
} else {
|
||||
STATE_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelRequest(index: Int) {
|
||||
mWorkerScope.cancelDecode(index)
|
||||
}
|
||||
|
||||
fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mWorkerScope.updateRAList(pages, pair)
|
||||
}
|
||||
|
||||
private fun request(index: Int, force: Boolean) {
|
||||
// Get page state
|
||||
val state = getPageState(index)
|
||||
|
||||
// Fix state for force
|
||||
if (force && (state == STATE_FINISHED || state == STATE_FAILED) || state == STATE_FAILED) {
|
||||
// Update state to none at once
|
||||
updatePageState(index, STATE_NONE)
|
||||
}
|
||||
mWorkerScope.launch(index, force)
|
||||
}
|
||||
|
||||
fun save(index: Int, file: UniFile): Boolean {
|
||||
val state = getPageState(index)
|
||||
return if (STATE_FINISHED != state) {
|
||||
false
|
||||
} else mSpiderDen.saveToUniFile(index, file)
|
||||
}
|
||||
|
||||
fun save(index: Int, dir: UniFile, filename: String): UniFile? {
|
||||
val state = getPageState(index)
|
||||
if (STATE_FINISHED != state) {
|
||||
return null
|
||||
}
|
||||
val ext = mSpiderDen.getExtension(index)
|
||||
val dst = dir.subFile(if (null != ext) "$filename.$ext" else filename) ?: return null
|
||||
return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst
|
||||
}
|
||||
|
||||
fun getExtension(index: Int): String? {
|
||||
val state = getPageState(index)
|
||||
return if (STATE_FINISHED != state) {
|
||||
null
|
||||
} else mSpiderDen.getExtension(index)
|
||||
}
|
||||
|
||||
val startPage: Int
|
||||
get() = mSpiderInfo.startPage
|
||||
|
||||
fun putStartPage(page: Int) {
|
||||
mSpiderInfo.startPage = page
|
||||
}
|
||||
|
||||
private fun readSpiderInfoFromLocal(): SpiderInfo? {
|
||||
return mSpiderDen.downloadDir?.run {
|
||||
findFile(SPIDER_INFO_FILENAME)?.let { file ->
|
||||
readCompatFromUniFile(file)?.takeIf {
|
||||
it.gid == galleryInfo.gid && it.token == galleryInfo.token
|
||||
}
|
||||
}
|
||||
}
|
||||
?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }
|
||||
}
|
||||
|
||||
@Throws(ParseException::class)
|
||||
private fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {
|
||||
spiderInfo.previewPages = parsePreviewPages(body)
|
||||
val previewSet = parsePreviewSet(body)
|
||||
if (previewSet.size() > 0) {
|
||||
if (index == 0) {
|
||||
spiderInfo.previewPerPage = previewSet.size()
|
||||
} else {
|
||||
spiderInfo.previewPerPage = previewSet.getPosition(0) / index
|
||||
}
|
||||
}
|
||||
var i = 0
|
||||
val n = previewSet.size()
|
||||
while (i < n) {
|
||||
val result = GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i))
|
||||
if (result != null) {
|
||||
spiderInfo.pTokenMap[result.page] = result.pToken
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {
|
||||
val request = EhRequestBuilder(
|
||||
getGalleryDetailUrl(
|
||||
galleryInfo.gid, galleryInfo.token, 0, false
|
||||
), referer
|
||||
).build()
|
||||
return runSuspendCatching {
|
||||
okHttpClient.newCall(request).executeAsync().use { response ->
|
||||
val body = response.body.string()
|
||||
val pages = parsePages(body)
|
||||
val spiderInfo = SpiderInfo(galleryInfo.gid, pages)
|
||||
spiderInfo.token = galleryInfo.token
|
||||
readPreviews(body, 0, spiderInfo)
|
||||
spiderInfo
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun getPTokenFromMultiPageViewer(index: Int): String? {
|
||||
val spiderInfo = mSpiderInfo
|
||||
val url = getGalleryMultiPageViewerUrl(
|
||||
galleryInfo.gid, galleryInfo.token!!
|
||||
)
|
||||
val referer = referer
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val body = response.body.string()
|
||||
val list = GalleryMultiPageViewerPTokenParser.parse(body)
|
||||
for (i in list.indices) {
|
||||
spiderInfo.pTokenMap[i] = list[i]
|
||||
}
|
||||
return spiderInfo.pTokenMap[index]
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getPTokenFromInternet(index: Int): String? {
|
||||
val spiderInfo = mSpiderInfo
|
||||
|
||||
// Check previewIndex
|
||||
var previewIndex: Int
|
||||
previewIndex = if (spiderInfo.previewPerPage >= 0) {
|
||||
index / spiderInfo.previewPerPage
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if (spiderInfo.previewPages > 0) {
|
||||
previewIndex = previewIndex.coerceAtMost(spiderInfo.previewPages - 1)
|
||||
}
|
||||
val url = getGalleryDetailUrl(
|
||||
galleryInfo.gid, galleryInfo.token, previewIndex, false
|
||||
)
|
||||
val referer = referer
|
||||
val request = EhRequestBuilder(url, referer).build()
|
||||
try {
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
val body = response.body.string()
|
||||
readPreviews(body, previewIndex, spiderInfo)
|
||||
return spiderInfo.pTokenMap[index]
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun writeSpiderInfoToLocal() {
|
||||
if (!isReady) return
|
||||
mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME).also { mSpiderInfo.write(it) } }
|
||||
mSpiderInfo.saveToCache()
|
||||
}
|
||||
|
||||
private fun isStateDone(state: Int): Boolean {
|
||||
return state == STATE_FINISHED || state == STATE_FAILED
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun updatePageState(index: Int, @State state: Int, error: String? = null) {
|
||||
var oldState: Int
|
||||
synchronized(mPageStateLock) {
|
||||
oldState = mPageStateArray[index]
|
||||
mPageStateArray[index] = state
|
||||
if (!isStateDone(oldState) && isStateDone(state)) {
|
||||
mDownloadedPages.incrementAndGet()
|
||||
} else if (isStateDone(oldState) && !isStateDone(state)) {
|
||||
mDownloadedPages.decrementAndGet()
|
||||
}
|
||||
if (oldState != STATE_FINISHED && state == STATE_FINISHED) {
|
||||
mFinishedPages.incrementAndGet()
|
||||
} else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {
|
||||
mFinishedPages.decrementAndGet()
|
||||
}
|
||||
|
||||
// Clear
|
||||
if (state == STATE_DOWNLOADING) {
|
||||
mPageErrorMap.remove(index)
|
||||
} else if (isStateDone(state)) {
|
||||
mPagePercentMap.remove(index)
|
||||
}
|
||||
|
||||
// Get default error
|
||||
if (state == STATE_FAILED) {
|
||||
mPageErrorMap[index] = error ?: GetText.getString(R.string.error_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
if (state == STATE_FAILED) {
|
||||
notifyPageFailure(index, error)
|
||||
} else if (state == STATE_FINISHED) {
|
||||
notifyPageSuccess(index)
|
||||
}
|
||||
if (mDownloadedPages.get() == size) notifyAllPageDownloaded()
|
||||
}
|
||||
|
||||
@IntDef(MODE_READ, MODE_DOWNLOAD)
|
||||
@Retention
|
||||
annotation class Mode
|
||||
|
||||
@IntDef(STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class State
|
||||
interface OnSpiderListener {
|
||||
fun onGetPages(pages: Int)
|
||||
fun onGet509(index: Int)
|
||||
fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int)
|
||||
fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int)
|
||||
fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int)
|
||||
fun onFinish(finished: Int, downloaded: Int, total: Int)
|
||||
fun onGetImageSuccess(index: Int, image: Image?)
|
||||
fun onGetImageFailure(index: Int, error: String?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MODE_READ = 0
|
||||
const val MODE_DOWNLOAD = 1
|
||||
const val STATE_NONE = 0
|
||||
const val STATE_DOWNLOADING = 1
|
||||
const val STATE_FINISHED = 2
|
||||
const val STATE_FAILED = 3
|
||||
const val SPIDER_INFO_FILENAME = ".ehviewer"
|
||||
private val sQueenMap = LongSparseArray<SpiderQueen>()
|
||||
|
||||
@JvmStatic
|
||||
fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {
|
||||
val gid = galleryInfo.gid
|
||||
return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {
|
||||
setMode(mode)
|
||||
launchIO { if (awaitReady()) updateMode() }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun releaseSpiderQueen(queen: SpiderQueen, @Mode mode: Int) {
|
||||
queen.run {
|
||||
clearMode(mode)
|
||||
if (mReadReference == 0 && mDownloadReference == 0) {
|
||||
stop()
|
||||
sQueenMap.remove(galleryInfo.gid)
|
||||
} else {
|
||||
launchIO { if (awaitReady()) updateMode() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mWorkerScope = object {
|
||||
private val mFetcherJobMap = hashMapOf<Int, Job>()
|
||||
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
|
||||
private val pTokenLock = Mutex()
|
||||
private var showKey: String? = null
|
||||
private val showKeyLock = Mutex()
|
||||
private val mDownloadDelay = Settings.getDownloadDelay()
|
||||
private var isDownloadMode = false
|
||||
|
||||
fun cancelDecode(index: Int) {
|
||||
decoder.cancel(index)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun enterDownloadMode() {
|
||||
if (isDownloadMode) return
|
||||
updateRAList((0 until size).toList())
|
||||
isDownloadMode = true
|
||||
}
|
||||
|
||||
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
|
||||
if (isDownloadMode) return
|
||||
synchronized(mFetcherJobMap) {
|
||||
mFetcherJobMap.forEach { (i, job) ->
|
||||
if (i < cancelBounds.first || i > cancelBounds.second)
|
||||
job.cancel()
|
||||
}
|
||||
list.forEach {
|
||||
if (mFetcherJobMap[it]?.isActive != true)
|
||||
doLaunchDownloadJob(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
|
||||
val state = mPageStateArray[index]
|
||||
if (!force && state == STATE_FINISHED) return
|
||||
val currentJob = mFetcherJobMap[index]
|
||||
if (force) currentJob?.cancel()
|
||||
if (currentJob?.isActive != true) {
|
||||
mFetcherJobMap[index] = launch {
|
||||
mSemaphore.withPermit {
|
||||
doInJob(index, force)
|
||||
}
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
if (getPageState(index) == STATE_DOWNLOADING) {
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image cancelled $index")
|
||||
mSpiderDen.remove(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(index: Int, force: Boolean = false) {
|
||||
check(index in 0 until size)
|
||||
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
|
||||
if (force) decoder.cancel(index)
|
||||
decoder.launch(index)
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int, force: Boolean) {
|
||||
fun getPToken(index: Int): String? {
|
||||
if (index !in 0 until size) return null
|
||||
return mSpiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
|
||||
?: getPTokenFromInternet(index)
|
||||
?: getPTokenFromInternet(index)
|
||||
?: getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
updatePageState(index, STATE_DOWNLOADING)
|
||||
if (!force && index in mSpiderDen) {
|
||||
return updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = mSpiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) mSpiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
pToken = getPToken(index) ?: return updatePageState(
|
||||
index,
|
||||
STATE_FAILED,
|
||||
PTOKEN_FAILED_MESSAGE
|
||||
).also {
|
||||
mSpiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
}
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
|
||||
var skipHathKey: String? = null
|
||||
val skipHathKeys = mutableListOf<String>()
|
||||
var originImageUrl: String? = null
|
||||
var error: String? = null
|
||||
var forceHtml = false
|
||||
var leakSkipHathKey = false
|
||||
repeat(3) {
|
||||
var imageUrl: String? = null
|
||||
var localShowKey: String?
|
||||
|
||||
showKeyLock.withLock {
|
||||
localShowKey = showKey
|
||||
if (localShowKey == null || forceHtml) {
|
||||
if (leakSkipHathKey) return@repeat
|
||||
var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
|
||||
// Add skipHathKey
|
||||
if (skipHathKey != null) {
|
||||
pageUrl += if ("?" in pageUrl) {
|
||||
"&nl=$skipHathKey"
|
||||
} else {
|
||||
"?nl=$skipHathKey"
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)
|
||||
.also {
|
||||
if (check509(it.imageUrl)) {
|
||||
// Get 509
|
||||
notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onSuccess { result ->
|
||||
imageUrl = result.imageUrl
|
||||
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
|
||||
originImageUrl = result.originImageUrl
|
||||
localShowKey = result.showKey
|
||||
|
||||
if (skipHathKey != null) {
|
||||
if (skipHathKey in skipHathKeys) {
|
||||
// Duplicate skip hath key
|
||||
leakSkipHathKey = true
|
||||
} else {
|
||||
skipHathKeys.add(skipHathKey!!)
|
||||
}
|
||||
} else {
|
||||
leakSkipHathKey = true
|
||||
}
|
||||
|
||||
showKey = result.showKey
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
return@repeat
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl == null) {
|
||||
if (localShowKey == null) {
|
||||
error = "ShowKey error"
|
||||
return@repeat
|
||||
}
|
||||
runSuspendCatching {
|
||||
EhEngine.getGalleryPageApi(
|
||||
mSpiderInfo.gid,
|
||||
index,
|
||||
pToken,
|
||||
localShowKey,
|
||||
previousPToken
|
||||
).also {
|
||||
if (check509(it.imageUrl)) {
|
||||
// Get 509
|
||||
notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
}
|
||||
return@repeat
|
||||
}.onSuccess {
|
||||
imageUrl = it.imageUrl
|
||||
skipHathKey = it.skipHathKey
|
||||
originImageUrl = it.originImageUrl
|
||||
}
|
||||
}
|
||||
|
||||
val targetImageUrl: String?
|
||||
val referer: String?
|
||||
|
||||
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
|
||||
targetImageUrl = originImageUrl
|
||||
referer = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)
|
||||
} else {
|
||||
targetImageUrl = imageUrl
|
||||
referer = null
|
||||
}
|
||||
if (targetImageUrl == null) {
|
||||
error = "TargetImageUrl error"
|
||||
return@repeat
|
||||
}
|
||||
Log.d(WORKER_DEBUG_TAG, targetImageUrl)
|
||||
|
||||
runSuspendCatching {
|
||||
Log.d(WORKER_DEBUG_TAG, "Start download image $index")
|
||||
val success: Boolean = mSpiderDen.makeHttpCallAndSaveImage(
|
||||
index,
|
||||
targetImageUrl,
|
||||
referer
|
||||
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
|
||||
mPagePercentMap[index] = receivedSize.toFloat() / contentLength
|
||||
notifyPageDownload(index, contentLength, receivedSize, bytesRead)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e(WORKER_DEBUG_TAG, "Can't download all of image data")
|
||||
error = "Incomplete"
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
if (mSpiderDen.checkPlainText(index)) {
|
||||
error = ""
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
Log.d(WORKER_DEBUG_TAG, "Download image succeed $index")
|
||||
|
||||
updatePageState(index, STATE_FINISHED)
|
||||
delay(mDownloadDelay.toLong())
|
||||
return
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
error = NETWORK_ERROR
|
||||
forceHtml = true
|
||||
}
|
||||
Log.d(WORKER_DEBUG_TAG, "End download image $index")
|
||||
}
|
||||
mSpiderDen.remove(index)
|
||||
updatePageState(index, STATE_FAILED, error)
|
||||
}
|
||||
|
||||
private val decoder = object {
|
||||
private val mSemaphore = Semaphore(4)
|
||||
private val mDecodeJobMap = hashMapOf<Int, Job>()
|
||||
|
||||
fun cancel(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
mDecodeJobMap.remove(index)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
val currentJob = mDecodeJobMap[index]
|
||||
if (currentJob?.isActive != true) {
|
||||
mDecodeJobMap[index] = launch {
|
||||
doInJob(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
val src = mSpiderDen.getImageSource(index) ?: return
|
||||
val image = mSemaphore.withPermit { Image.decode(src) }
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image?.recycle()
|
||||
throw it
|
||||
}
|
||||
if (image == null) {
|
||||
notifyGetImageFailure(index, DECODE_ERROR)
|
||||
} else {
|
||||
notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)
|
||||
private val ERROR_509 = GetText.getString(R.string.error_509)
|
||||
private val NETWORK_ERROR = GetText.getString(R.string.error_socket)
|
||||
private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)
|
||||
private val URL_509_ARRAY = arrayOf(
|
||||
"https://ehgt.org/g/509.gif",
|
||||
"https://ehgt.org/g/509s.gif",
|
||||
"https://exhentai.org/img/509.gif",
|
||||
"https://exhentai.org/img/509s.gif"
|
||||
)
|
||||
private const val WORKER_DEBUG_TAG = "SpiderQueenWorker"
|
||||
|
||||
private fun check509(url: String) = url in URL_509_ARRAY
|
||||
@@ -1,328 +0,0 @@
|
||||
package com.hippo.ehviewer.spider
|
||||
|
||||
import android.util.Log
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine.getGalleryPage
|
||||
import com.hippo.ehviewer.client.EhEngine.getGalleryPageApi
|
||||
import com.hippo.ehviewer.client.EhUrl.getPageUrl
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.DECODE_ERROR
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.ERROR_509
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.NETWORK_ERROR
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.PTOKEN_FAILED_MESSAGE
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FAILED
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.STATE_FINISHED
|
||||
import com.hippo.image.Image.Companion.decode
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.yorozuya.StringUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class SpiderQueenWorker(private val queen: SpiderQueen) : CoroutineScope {
|
||||
private val spiderDen
|
||||
get() = queen.mSpiderDen
|
||||
private val spiderInfo by lazy { queen.mSpiderInfo.get() }
|
||||
private val mFetcherJobMap = hashMapOf<Int, Job>()
|
||||
private val mSemaphore = Semaphore(Settings.getMultiThreadDownload())
|
||||
private val pTokenLock = Mutex()
|
||||
private var showKey: String? = null
|
||||
private val showKeyLock = Mutex()
|
||||
private val mDownloadDelay = Settings.getDownloadDelay()
|
||||
private var isDownloadMode = false
|
||||
private val size
|
||||
get() = queen.size()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.IO + Job()
|
||||
|
||||
fun cancelDecode(index: Int) {
|
||||
decoder.cancel(index)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun enterDownloadMode() {
|
||||
if (isDownloadMode) return
|
||||
updateRAList((0 until size).toList())
|
||||
isDownloadMode = true
|
||||
}
|
||||
|
||||
fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {
|
||||
if (isDownloadMode) return
|
||||
synchronized(mFetcherJobMap) {
|
||||
mFetcherJobMap.forEach { (i, job) ->
|
||||
if (i < cancelBounds.first || i > cancelBounds.second)
|
||||
job.cancel()
|
||||
}
|
||||
list.forEach {
|
||||
if (mFetcherJobMap[it]?.isActive != true)
|
||||
doLaunchDownloadJob(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLaunchDownloadJob(index: Int, force: Boolean) {
|
||||
val state = queen.mPageStateArray[index]
|
||||
if (!force && state == STATE_FINISHED) return
|
||||
val currentJob = mFetcherJobMap[index]
|
||||
if (force) currentJob?.cancel()
|
||||
if (currentJob?.isActive != true) {
|
||||
mFetcherJobMap[index] = launch {
|
||||
mSemaphore.withPermit {
|
||||
doInJob(index, force)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(index: Int, force: Boolean = false) {
|
||||
check(index in 0 until size)
|
||||
if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }
|
||||
decoder.launch(index)
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int, force: Boolean) {
|
||||
fun getPToken(index: Int): String? {
|
||||
if (index !in 0 until size) return null
|
||||
return spiderInfo.pTokenMap[index].takeIf { it != TOKEN_FAILED }
|
||||
?: queen.getPTokenFromInternet(index)
|
||||
?: queen.getPTokenFromInternet(index)
|
||||
?: queen.getPTokenFromMultiPageViewer(index)
|
||||
}
|
||||
queen.updatePageState(index, SpiderQueen.STATE_DOWNLOADING)
|
||||
if (!force && index in spiderDen) {
|
||||
return queen.updatePageState(index, STATE_FINISHED)
|
||||
}
|
||||
if (force) {
|
||||
pTokenLock.withLock {
|
||||
val pToken = spiderInfo.pTokenMap[index]
|
||||
if (pToken == TOKEN_FAILED) spiderInfo.pTokenMap.remove(index)
|
||||
}
|
||||
}
|
||||
val previousPToken: String?
|
||||
val pToken: String
|
||||
|
||||
pTokenLock.withLock {
|
||||
pToken = getPToken(index) ?: return queen.updatePageState(
|
||||
index,
|
||||
STATE_FAILED,
|
||||
PTOKEN_FAILED_MESSAGE
|
||||
).also {
|
||||
spiderInfo.pTokenMap[index] = TOKEN_FAILED
|
||||
}
|
||||
previousPToken = getPToken(index - 1)
|
||||
}
|
||||
|
||||
var skipHathKey: String? = null
|
||||
val skipHathKeys = mutableListOf<String>()
|
||||
var originImageUrl: String? = null
|
||||
var error: String? = null
|
||||
var forceHtml = false
|
||||
var leakSkipHathKey = false
|
||||
repeat(3) {
|
||||
var imageUrl: String? = null
|
||||
var localShowKey: String?
|
||||
|
||||
showKeyLock.withLock {
|
||||
localShowKey = showKey
|
||||
if (localShowKey == null || forceHtml) {
|
||||
if (leakSkipHathKey) return@repeat
|
||||
var pageUrl = getPageUrl(spiderInfo.gid, index, pToken)
|
||||
// Add skipHathKey
|
||||
if (skipHathKey != null) {
|
||||
pageUrl += if ("?" in pageUrl) {
|
||||
"&nl=$skipHathKey"
|
||||
} else {
|
||||
"?nl=$skipHathKey"
|
||||
}
|
||||
}
|
||||
runSuspendCatching {
|
||||
getGalleryPage(pageUrl, spiderInfo.gid, spiderInfo.token).also {
|
||||
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
|
||||
// Get 509
|
||||
queen.notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onSuccess { result ->
|
||||
imageUrl = result.imageUrl
|
||||
skipHathKey = result.skipHathKey.takeIf { it.isNotBlank() }
|
||||
originImageUrl = result.originImageUrl
|
||||
localShowKey = result.showKey
|
||||
|
||||
if (skipHathKey != null) {
|
||||
if (skipHathKey in skipHathKeys) {
|
||||
// Duplicate skip hath key
|
||||
leakSkipHathKey = true
|
||||
} else {
|
||||
skipHathKeys.add(skipHathKey!!)
|
||||
}
|
||||
} else {
|
||||
leakSkipHathKey = true
|
||||
}
|
||||
|
||||
showKey = result.showKey
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
return@repeat
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl == null) {
|
||||
if (localShowKey == null) {
|
||||
error = "ShowKey error"
|
||||
return@repeat
|
||||
}
|
||||
runSuspendCatching {
|
||||
getGalleryPageApi(
|
||||
spiderInfo.gid,
|
||||
index,
|
||||
pToken,
|
||||
localShowKey,
|
||||
previousPToken
|
||||
).also {
|
||||
if (StringUtils.endsWith(it.imageUrl, URL_509_SUFFIX_ARRAY)) {
|
||||
// Get 509
|
||||
queen.notifyGet509(index)
|
||||
error = ERROR_509
|
||||
return@repeat
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
if (it is ParseException && "Key mismatch" == it.message) {
|
||||
// Show key is wrong, enter a new loop to get the new show key
|
||||
if (showKey == localShowKey) showKey = null
|
||||
} else {
|
||||
error = ExceptionUtils.getReadableString(it)
|
||||
}
|
||||
return@repeat
|
||||
}.onSuccess {
|
||||
imageUrl = it.imageUrl
|
||||
skipHathKey = it.skipHathKey
|
||||
originImageUrl = it.originImageUrl
|
||||
}
|
||||
}
|
||||
|
||||
val targetImageUrl: String?
|
||||
val referer: String?
|
||||
|
||||
if (Settings.getDownloadOriginImage() && !originImageUrl.isNullOrBlank()) {
|
||||
targetImageUrl = originImageUrl
|
||||
referer = getPageUrl(spiderInfo.gid, index, pToken)
|
||||
} else {
|
||||
targetImageUrl = imageUrl
|
||||
referer = null
|
||||
}
|
||||
if (targetImageUrl == null) {
|
||||
error = "TargetImageUrl error"
|
||||
return@repeat
|
||||
}
|
||||
Log.d(DEBUG_TAG, targetImageUrl)
|
||||
|
||||
runSuspendCatching {
|
||||
Log.d(DEBUG_TAG, "Start download image $index")
|
||||
val success: Boolean = spiderDen.makeHttpCallAndSaveImage(
|
||||
index,
|
||||
targetImageUrl,
|
||||
referer
|
||||
) { contentLength: Long, receivedSize: Long, bytesRead: Int ->
|
||||
queen.mPagePercentMap[index] = receivedSize.toFloat() / contentLength
|
||||
queen.notifyPageDownload(index, contentLength, receivedSize, bytesRead)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Log.e(DEBUG_TAG, "Can't download all of image data")
|
||||
error = "Incomplete"
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
if (spiderDen.checkPlainText(index)) {
|
||||
error = ""
|
||||
forceHtml = true
|
||||
return@repeat
|
||||
}
|
||||
|
||||
Log.d(DEBUG_TAG, "Download image succeed $index")
|
||||
|
||||
queen.updatePageState(index, STATE_FINISHED)
|
||||
delay(mDownloadDelay.toLong())
|
||||
return
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
error = NETWORK_ERROR
|
||||
}
|
||||
Log.d(DEBUG_TAG, "End download image $index")
|
||||
}
|
||||
spiderDen.remove(index)
|
||||
queen.updatePageState(index, STATE_FAILED, error)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val URL_509_SUFFIX_ARRAY = arrayOf(
|
||||
"/509.gif",
|
||||
"/509s.gif"
|
||||
)
|
||||
|
||||
private const val DEBUG_TAG = "SpiderQueenWorker"
|
||||
}
|
||||
|
||||
private val decoder = Decoder()
|
||||
|
||||
inner class Decoder {
|
||||
private val mSemaphore = Semaphore(4)
|
||||
private val mDecodeJobMap = hashMapOf<Int, Job>()
|
||||
|
||||
fun cancel(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
mDecodeJobMap.remove(index)?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(index: Int) {
|
||||
synchronized(mDecodeJobMap) {
|
||||
val currentJob = mDecodeJobMap[index]
|
||||
if (currentJob?.isActive != true) {
|
||||
mDecodeJobMap[index] = launch {
|
||||
doInJob(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doInJob(index: Int) {
|
||||
mFetcherJobMap[index]?.takeIf { it.isActive }?.join()
|
||||
val src = spiderDen.getImageSource(index) ?: return
|
||||
val image = mSemaphore.withPermit { decode(src) }
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image?.recycle()
|
||||
throw it
|
||||
}
|
||||
if (image == null) {
|
||||
queen.notifyGetImageFailure(index, DECODE_ERROR)
|
||||
} else {
|
||||
queen.notifyGetImageSuccess(index, image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.hippo.app.ListCheckBoxDialogBuilder
|
||||
import com.hippo.ehviewer.EhApplication.Companion.application
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
@@ -89,8 +89,8 @@ import java.io.OutputStream
|
||||
class MainActivity : EhActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var connectivityManager: ConnectivityManager
|
||||
private val availableNetworks: MutableList<Network> = mutableListOf()
|
||||
private val connectivityManager by lazy { getSystemService<ConnectivityManager>()!! }
|
||||
private val availableNetworks = mutableListOf<Network>()
|
||||
|
||||
private fun saveImageToTempFile(uri: Uri): File? {
|
||||
val src = ImageDecoder.createSource(contentResolver, uri)
|
||||
@@ -223,7 +223,6 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
connectivityManager = getSystemService()!!
|
||||
if (Settings.getDF() && Settings.getBypassVpn()) {
|
||||
bypassVpn()
|
||||
}
|
||||
@@ -233,13 +232,7 @@ class MainActivity : EhActivity() {
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
if (EhUtils.needSignedIn()) {
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
setNavGraph()
|
||||
}.launch(Intent(this, LoginActivity::class.java))
|
||||
} else {
|
||||
setNavGraph()
|
||||
}
|
||||
if (!EhUtils.needSignedIn()) setNavGraph()
|
||||
binding.drawView.addDrawerListener(mDrawerOnBackPressedCallback)
|
||||
binding.navView.setupWithNavController(navController)
|
||||
|
||||
@@ -371,7 +364,19 @@ class MainActivity : EhActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val loginLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (availableNetworks.isNotEmpty()) {
|
||||
connectivityManager.bindProcessToNetwork(availableNetworks.last())
|
||||
}
|
||||
setNavGraph()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (EhUtils.needSignedIn()) {
|
||||
connectivityManager.bindProcessToNetwork(null)
|
||||
loginLauncher.launch(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
super.onResume()
|
||||
lifecycleScope.launch {
|
||||
delay(100)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -41,10 +40,11 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser
|
||||
import com.hippo.ehviewer.ui.scene.BaseScene
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.util.IoThreadPoolExecutor
|
||||
import com.hippo.util.LogCat
|
||||
import com.hippo.util.ReadableTime
|
||||
import com.hippo.yorozuya.IOUtils
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -79,10 +79,9 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show()
|
||||
IoThreadPoolExecutor.getInstance().execute {
|
||||
lifecycleScope.launchIO {
|
||||
val success = EhDB.exportDB(requireActivity(), uri)
|
||||
val activity: Activity? = activity
|
||||
activity?.runOnUiThread {
|
||||
withUIContext {
|
||||
if (alertDialog.isShowing) {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
@@ -204,10 +203,9 @@ class AdvancedFragment : BasePreferenceFragment() {
|
||||
.setCancelable(false)
|
||||
.setView(R.layout.preference_dialog_task)
|
||||
.show()
|
||||
IoThreadPoolExecutor.getInstance().execute {
|
||||
lifecycleScope.launchIO {
|
||||
val error = EhDB.importDB(requireActivity(), uri)
|
||||
val activity: Activity? = activity
|
||||
activity?.runOnUiThread {
|
||||
withUIContext {
|
||||
if (alertDialog.isShowing) {
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
|
||||
@@ -23,12 +23,17 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat.CLOCK_12H
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.dailycheck.schedHour
|
||||
import com.hippo.ehviewer.dailycheck.schedMinute
|
||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
|
||||
class EhFragment : BasePreferenceFragment() {
|
||||
@@ -57,6 +62,7 @@ class EhFragment : BasePreferenceFragment() {
|
||||
thumbResolution!!.setSummaryProvider {
|
||||
getString(R.string.settings_eh_thumb_resolution_summary, (it as ListPreference).entry)
|
||||
}
|
||||
|
||||
if (!EhTagDatabase.isTranslatable(requireActivity())) {
|
||||
preferenceScreen.removePreference(showTagTranslations)
|
||||
preferenceScreen.removePreference(tagTranslationsSource!!)
|
||||
@@ -66,6 +72,27 @@ class EhFragment : BasePreferenceFragment() {
|
||||
val preference = findPreference<Preference>(it)
|
||||
preferenceScreen.removePreference(preference!!)
|
||||
}
|
||||
} else {
|
||||
findPreference<Preference>(Settings.KEY_REQUEST_NEWS_TIMER)!!.apply {
|
||||
setOnPreferenceClickListener {
|
||||
MaterialTimePicker.Builder()
|
||||
.apply {
|
||||
schedHour?.let { setHour(it) }
|
||||
schedMinute?.let { setMinute(it) }
|
||||
}
|
||||
.setTimeFormat(CLOCK_12H)
|
||||
.build()
|
||||
.apply {
|
||||
addOnPositiveButtonClickListener {
|
||||
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_HOUR, hour)
|
||||
Settings.putInt(Settings.KEY_REQUEST_NEWS_TIMER_MINUTE, minute)
|
||||
updateDailyCheckWork(requireContext())
|
||||
}
|
||||
}
|
||||
.show(childFragmentManager, null)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +132,10 @@ class EhFragment : BasePreferenceFragment() {
|
||||
EhApplication.application.recreateAllActivity()
|
||||
}
|
||||
return true
|
||||
} else if (Settings.KEY_REQUEST_NEWS == key) {
|
||||
updateDailyCheckWork(requireContext())
|
||||
} else if (Settings.KEY_REQUEST_NEWS_TIMER == key) {
|
||||
updateDailyCheckWork(requireContext())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ class FilterFragment : BaseFragment() {
|
||||
mEhFilter.deleteFilter(filter)
|
||||
}
|
||||
|
||||
fun trigger(filter: Filter?) {
|
||||
fun trigger(filter: Filter) {
|
||||
mEhFilter.triggerFilter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,10 @@ suspend fun postLogin() = coroutineScope {
|
||||
}
|
||||
launch {
|
||||
runCatching {
|
||||
// For the `star` cookie
|
||||
EhEngine.getNews(false)
|
||||
|
||||
// Sad panda check
|
||||
Settings.putGallerySite(EhUrl.SITE_EX)
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
|
||||
@@ -51,7 +51,7 @@ import com.hippo.easyrecyclerview.FastScroller
|
||||
import com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener
|
||||
import com.hippo.easyrecyclerview.HandlerDrawable
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -70,13 +70,13 @@ 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
|
||||
import com.hippo.yorozuya.collect.LongList
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import rikka.core.res.resolveColor
|
||||
@@ -151,8 +151,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
mDownloadManager = downloadManager
|
||||
mDownloadManager!!.addDownloadInfoListener(this)
|
||||
if (savedInstanceState == null) {
|
||||
@@ -248,10 +246,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
container!!.addView(mFabLayout)
|
||||
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
|
||||
mViewTransition = ViewTransition(content, mTip)
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(content)
|
||||
val resources = context!!.resources
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.big_download)
|
||||
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_download)
|
||||
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
mTip!!.setCompoundDrawables(null, drawable, null, null)
|
||||
mAdapter = DownloadAdapter()
|
||||
@@ -377,7 +372,9 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
if (mDownloadManager != null) {
|
||||
mDownloadManager!!.resetAllReadingProgress()
|
||||
lifecycleScope.launchNonCancellable {
|
||||
mDownloadManager!!.resetAllReadingProgress()
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
return true
|
||||
@@ -419,8 +416,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
val view = inflater.inflate(R.layout.drawer_list_rv, container, false)
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.setTitle(R.string.download_labels)
|
||||
toolbar.inflateMenu(R.menu.drawer_download)
|
||||
@@ -428,7 +423,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
val id = item.itemId
|
||||
if (id == R.id.action_add) {
|
||||
val builder =
|
||||
EditTextDialogBuilder(context!!, null, getString(R.string.download_labels))
|
||||
EditTextDialogBuilder(requireContext(), null, getString(R.string.download_labels))
|
||||
builder.setTitle(R.string.new_label_title)
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
val dialog = builder.show()
|
||||
@@ -446,7 +441,7 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
items[i + 2] = list[i].label
|
||||
i++
|
||||
}
|
||||
BaseDialogBuilder(context!!)
|
||||
BaseDialogBuilder(requireContext())
|
||||
.setTitle(R.string.default_download_label)
|
||||
.setItems(items) { _: DialogInterface?, which: Int ->
|
||||
if (which == 0) {
|
||||
@@ -991,7 +986,6 @@ class DownloadsScene : BaseToolbarScene(), DownloadInfoListener, OnClickFabListe
|
||||
private val mListThumbHeight: Int
|
||||
|
||||
init {
|
||||
AssertUtils.assertNotNull(mInflater)
|
||||
@SuppressLint("InflateParams") val calculator =
|
||||
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
|
||||
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
@@ -63,7 +63,6 @@ import com.hippo.widget.ContentLayout
|
||||
import com.hippo.widget.FabLayout
|
||||
import com.hippo.widget.FabLayout.OnClickFabListener
|
||||
import com.hippo.widget.FabLayout.OnExpandListener
|
||||
import com.hippo.yorozuya.AssertUtils
|
||||
import com.hippo.yorozuya.ObjectUtils
|
||||
import com.hippo.yorozuya.SimpleHandler
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
@@ -132,8 +131,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
private var mModifyAdd = false
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
mClient = EhClient
|
||||
mFavLocalCount = Settings.getFavLocalCount()
|
||||
mFavCountSum = Settings.getFavCloudCount()
|
||||
@@ -186,7 +183,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
val view = inflater.inflate(R.layout.scene_favorites, container, false)
|
||||
val mContentLayout = view.findViewById<ContentLayout>(R.id.content_layout)
|
||||
val activity = mainActivity!!
|
||||
AssertUtils.assertNotNull(activity)
|
||||
setOnApplySearch { query: String? ->
|
||||
onApplySearch(query)
|
||||
}
|
||||
@@ -196,7 +192,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout
|
||||
mFabLayout!!.addOnExpandListener(FabLayoutListener())
|
||||
(mFabLayout!!.parent as ViewGroup).removeView(mFabLayout)
|
||||
AssertUtils.assertNotNull(container)
|
||||
container!!.addView(mFabLayout)
|
||||
ViewCompat.setWindowInsetsAnimationCallback(
|
||||
view, WindowInsetsAnimationHelper(
|
||||
@@ -204,9 +199,6 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
mFabLayout
|
||||
)
|
||||
)
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
val resources = context!!.resources
|
||||
val paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)
|
||||
mHelper = FavoritesHelper()
|
||||
mHelper!!.setEmptyString(resources.getString(R.string.gallery_list_empty_hit))
|
||||
@@ -332,9 +324,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.drawer_list_rv, container, false)
|
||||
val context = context
|
||||
val toolbar = ViewUtils.`$$`(view, R.id.toolbar) as Toolbar
|
||||
AssertUtils.assertNotNull(context)
|
||||
toolbar.setTitle(R.string.collections)
|
||||
toolbar.inflateMenu(R.menu.drawer_favorites)
|
||||
toolbar.setOnMenuItemClickListener { item: MenuItem ->
|
||||
@@ -345,7 +335,7 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
items[1] = getString(R.string.local_favorites)
|
||||
val favCat = Settings.getFavCat()
|
||||
System.arraycopy(favCat, 0, items, 2, 10)
|
||||
BaseDialogBuilder(context!!)
|
||||
BaseDialogBuilder(requireContext())
|
||||
.setTitle(R.string.default_favorites_collection)
|
||||
.setItems(items) { _: DialogInterface?, which: Int ->
|
||||
Settings.putDefaultFavSlot(
|
||||
@@ -656,8 +646,8 @@ class FavoritesScene : SearchBarScene(), OnDragHandlerListener, OnClickFabListen
|
||||
|
||||
private fun onGetFavoritesLocal(keyword: String?, taskId: Int) {
|
||||
if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {
|
||||
val list: List<GalleryInfo> = if (TextUtils.isEmpty(keyword)) {
|
||||
EhDB.getAllLocalFavorites()
|
||||
val list: List<GalleryInfo> = if (keyword.isNullOrEmpty()) {
|
||||
EhDB.allLocalFavorites
|
||||
} else {
|
||||
EhDB.searchLocalFavorites(keyword)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.hippo.drawable.TriangleDrawable
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
|
||||
@@ -253,7 +253,7 @@ class GalleryCommentsScene : BaseToolbarScene(), View.OnClickListener, OnRefresh
|
||||
text.clearSpan(start, end, false)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> return false
|
||||
}
|
||||
mode?.finish()
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ import com.hippo.app.BaseDialogBuilder
|
||||
import com.hippo.app.CheckBoxDialogBuilder
|
||||
import com.hippo.app.EditTextDialogBuilder
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ehCookieStore
|
||||
import com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
@@ -89,6 +89,7 @@ import com.hippo.ehviewer.databinding.SceneGalleryDetailBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
|
||||
import com.hippo.ehviewer.spider.SpiderDen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.GalleryInfoBottomSheet
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
@@ -106,7 +107,6 @@ import com.hippo.widget.AutoWrapLayout
|
||||
import com.hippo.widget.LoadImageView
|
||||
import com.hippo.widget.ObservedTextView
|
||||
import com.hippo.widget.SimpleGridAutoSpanLayout
|
||||
import com.hippo.yorozuya.AssertUtils
|
||||
import com.hippo.yorozuya.FileUtils
|
||||
import com.hippo.yorozuya.IntIdGenerator
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
@@ -264,7 +264,9 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
mGalleryInfo?.let {
|
||||
viewLifecycleOwner.lifecycleScope.launchIO {
|
||||
runCatching {
|
||||
val startPage = SpiderQueen.getStartPage(it.gid)
|
||||
val queen = SpiderQueen.obtainSpiderQueen(it, MODE_READ)
|
||||
val startPage = queen.awaitStartPage()
|
||||
SpiderQueen.releaseSpiderQueen(queen, MODE_READ)
|
||||
withUIContext {
|
||||
binding.content.header.read.text = if (startPage == 0) {
|
||||
getString(R.string.read)
|
||||
@@ -361,8 +363,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
// Get download state
|
||||
val gid = gid
|
||||
mDownloadState = if (gid != -1L) {
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
downloadManager.getDownloadState(gid)
|
||||
} else {
|
||||
DownloadInfo.STATE_INVALID
|
||||
@@ -426,8 +426,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
downloadManager.removeDownloadInfoListener(this)
|
||||
(requireActivity() as MainActivity).mShareUrl = null
|
||||
mViewTransition = null
|
||||
@@ -436,8 +434,6 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
}
|
||||
|
||||
private fun prepareData(): Boolean {
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
if (mGalleryDetail != null) {
|
||||
return true
|
||||
}
|
||||
@@ -451,7 +447,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (mGalleryDetail != null) {
|
||||
return true
|
||||
}
|
||||
val application = context!!.applicationContext as EhApplication
|
||||
val application = requireContext().applicationContext as EhApplication
|
||||
return if (application.containGlobalStuff(mRequestId)) {
|
||||
// request exist
|
||||
true
|
||||
@@ -574,7 +570,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
_binding ?: return
|
||||
val resources = resources
|
||||
binding.content.header.run {
|
||||
(thumb as LoadImageView).load(gd.thumb!!, true)
|
||||
(thumb as LoadImageView).load(gd.thumb!!, false)
|
||||
setTitle(EhUtils.getSuitableTitle(gd))
|
||||
uploader.text = gd.uploader
|
||||
if (gd.disowned) {
|
||||
@@ -652,7 +648,8 @@ 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('_')) {
|
||||
tag.alpha = 0.5f
|
||||
val alpha = ColorUtils.setAlphaComponent(tag.currentTextColor, 128)
|
||||
tag.setTextColor(alpha)
|
||||
it.substring(1)
|
||||
} else {
|
||||
it
|
||||
@@ -729,14 +726,14 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
for (i in 0 until previewSet.size()) {
|
||||
val view = inflater.inflate(R.layout.item_gallery_preview, gridLayout, false)
|
||||
val image = view.findViewById<LoadImageView>(R.id.image)
|
||||
previewSet.load(image, gd.gid, i)
|
||||
image.setTag(R.id.index, i)
|
||||
withUIContext {
|
||||
gridLayout.addView(view)
|
||||
image.setTag(R.id.index, i)
|
||||
image.setOnClickListener(this@GalleryDetailScene)
|
||||
val text = view.findViewById<TextView>(R.id.text)
|
||||
text.text = (previewSet.getPosition(i) + 1).toString()
|
||||
}
|
||||
previewSet.load(image, gd.gid, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1197,7 +1194,7 @@ class GalleryDetailScene : CollapsingToolbarScene(), View.OnClickListener, Downl
|
||||
if (null == context || -1L == gid) {
|
||||
return
|
||||
}
|
||||
val downloadState = downloadManager.getDownloadState(gid)
|
||||
val downloadState = com.hippo.ehviewer.download.DownloadManager.getDownloadState(gid)
|
||||
if (downloadState == mDownloadState) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import com.hippo.app.EditTextDialogBuilder
|
||||
import com.hippo.drawable.AddDeleteDrawable
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView
|
||||
import com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener
|
||||
import com.hippo.ehviewer.EhApplication.Companion.downloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager as downloadManager
|
||||
import com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.FavouriteStatusRouter
|
||||
@@ -74,7 +74,9 @@ import com.hippo.ehviewer.client.EhRequest
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_NORMAL
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TAG
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
@@ -98,13 +100,14 @@ import com.hippo.widget.FabLayout
|
||||
import com.hippo.widget.FabLayout.OnClickFabListener
|
||||
import com.hippo.widget.FabLayout.OnExpandListener
|
||||
import com.hippo.yorozuya.AnimationUtils
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import com.hippo.yorozuya.SimpleAnimatorListener
|
||||
import com.hippo.yorozuya.StringUtils
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import kotlinx.coroutines.delay
|
||||
import rikka.core.res.resolveColor
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
@@ -164,7 +167,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
}
|
||||
}
|
||||
private var mHasFirstRefresh = false
|
||||
private var mNavCheckedId = 0
|
||||
private val mDownloadManager: DownloadManager = downloadManager
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@@ -211,11 +213,19 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)
|
||||
ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)
|
||||
ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = "11")
|
||||
ACTION_LIST_URL_BUILDER -> args?.getParcelable<ListUrlBuilder>(KEY_LIST_URL_BUILDER)
|
||||
?.copy() ?: ListUrlBuilder()
|
||||
ACTION_LIST_URL_BUILDER ->
|
||||
args?.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)?.copy()
|
||||
?: ListUrlBuilder()
|
||||
|
||||
else -> throw IllegalStateException("Wrong KEY_ACTION:${args?.getString(KEY_ACTION)} when handle args!")
|
||||
}
|
||||
val goto = args?.getString(KEY_GOTO)
|
||||
goto?.let {
|
||||
lifecycleScope.launchUI {
|
||||
delay(100)
|
||||
mHelper?.goTo(it, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -288,25 +298,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
})
|
||||
}
|
||||
|
||||
private fun wrapTagKeyword(keyword: String?): String {
|
||||
var keyword = keyword
|
||||
keyword = keyword!!.trim { it <= ' ' }
|
||||
val index1 = keyword.indexOf(':')
|
||||
if (index1 == -1 || index1 >= keyword.length - 1) {
|
||||
// Can't find :, or : is the last char
|
||||
return keyword
|
||||
}
|
||||
if (keyword[index1 + 1] == '"') {
|
||||
// The char after : is ", the word must be quoted
|
||||
return keyword
|
||||
}
|
||||
val index2 = keyword.indexOf(' ')
|
||||
return if (index2 <= index1) {
|
||||
// Can't find space, or space is before :
|
||||
keyword
|
||||
} else keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\""
|
||||
}
|
||||
|
||||
// Update search bar title, drawer checked item
|
||||
private fun onUpdateUrlBuilder() {
|
||||
_binding ?: return
|
||||
@@ -324,13 +315,16 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(0, !isPopular)
|
||||
binding.fabLayout.setSecondaryFabVisibilityAt(2, !isPopular)
|
||||
|
||||
// Update normal search mode
|
||||
// Update normal search mode and category
|
||||
binding.searchLayout.setNormalSearchMode(if (mode == MODE_SUBSCRIPTION) R.id.search_subscription_search else R.id.search_normal_search)
|
||||
if (category != EhUtils.NONE) {
|
||||
binding.searchLayout.setCategory(category)
|
||||
}
|
||||
|
||||
// Update search edit text
|
||||
if (!mIsTopList) {
|
||||
if (mode == ListUrlBuilder.MODE_TAG) {
|
||||
keyword = wrapTagKeyword(keyword)
|
||||
if (mode == MODE_TAG) {
|
||||
keyword = wrapTagKeyword(keyword!!)
|
||||
}
|
||||
setSearchBarText(keyword)
|
||||
}
|
||||
@@ -543,14 +537,15 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
val qsDrawerAdapter = QsDrawerAdapter(inflater)
|
||||
qsDrawerAdapter.setHasStableIds(true)
|
||||
drawerBinding.recyclerViewDrawer.adapter = qsDrawerAdapter
|
||||
val itemTouchHelper = ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
|
||||
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
|
||||
if (!mIsTopList) {
|
||||
val itemTouchHelper =
|
||||
ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))
|
||||
itemTouchHelper.attachToRecyclerView(drawerBinding.recyclerViewDrawer)
|
||||
drawerBinding.tip.visibility = View.VISIBLE
|
||||
drawerBinding.recyclerViewDrawer.visibility = View.GONE
|
||||
}
|
||||
lifecycleScope.launchIO {
|
||||
mQuickSearchList = EhDB.getAllQuickSearch()
|
||||
mQuickSearchList = EhDB.allQuickSearch.toMutableList()
|
||||
if (mQuickSearchList.isNotEmpty()) {
|
||||
withUIContext {
|
||||
drawerBinding.tip.visibility = View.GONE
|
||||
@@ -992,7 +987,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
val oldMode = mUrlBuilder.mode
|
||||
// If it's MODE_SUBSCRIPTION, keep it
|
||||
val newMode =
|
||||
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else ListUrlBuilder.MODE_NORMAL
|
||||
if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else MODE_NORMAL
|
||||
mUrlBuilder.reset()
|
||||
mUrlBuilder.mode = newMode
|
||||
mUrlBuilder.keyword = query
|
||||
@@ -1152,8 +1147,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
val goto = if (i != -1) q.name!!.substring(i + 1) else null
|
||||
val args = ListUrlBuilder().apply {
|
||||
set(q)
|
||||
setIndex(goto)
|
||||
}.toStartArgs()
|
||||
}.toStartArgs().apply { putString(KEY_GOTO, goto) }
|
||||
navigate(R.id.galleryListScene, args, true)
|
||||
setState(STATE_NORMAL)
|
||||
closeDrawer(GravityCompat.END)
|
||||
@@ -1428,6 +1422,7 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
|
||||
companion object {
|
||||
const val KEY_ACTION = "action"
|
||||
const val KEY_GOTO = "goto"
|
||||
const val ACTION_HOMEPAGE = "action_homepage"
|
||||
const val ACTION_SUBSCRIPTION = "action_subscription"
|
||||
const val ACTION_WHATS_HOT = "action_whats_hot"
|
||||
@@ -1441,38 +1436,6 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
private const val STATE_SEARCH = 2
|
||||
private const val STATE_SEARCH_SHOW_LIST = 3
|
||||
private const val ANIMATE_TIME = 300L
|
||||
private fun getSuitableTitleForUrlBuilder(
|
||||
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
|
||||
): String? {
|
||||
val keyword = urlBuilder.keyword
|
||||
val category = urlBuilder.category
|
||||
return if (ListUrlBuilder.MODE_NORMAL == urlBuilder.mode && EhUtils.NONE == category &&
|
||||
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
|
||||
) {
|
||||
resources.getString(if (appName) R.string.app_name else R.string.homepage)
|
||||
} else if (MODE_SUBSCRIPTION == urlBuilder.mode && EhUtils.NONE == category &&
|
||||
TextUtils.isEmpty(keyword) && urlBuilder.advanceSearch == -1 && urlBuilder.minRating == -1 && urlBuilder.pageFrom == -1 && urlBuilder.pageTo == -1
|
||||
) {
|
||||
resources.getString(R.string.subscription)
|
||||
} else if (MODE_WHATS_HOT == urlBuilder.mode) {
|
||||
resources.getString(R.string.whats_hot)
|
||||
} else if (MODE_TOPLIST == urlBuilder.mode) {
|
||||
when (urlBuilder.keyword) {
|
||||
"11" -> resources.getString(R.string.toplist_alltime)
|
||||
"12" -> resources.getString(R.string.toplist_pastyear)
|
||||
"13" -> resources.getString(R.string.toplist_pastmonth)
|
||||
"15" -> resources.getString(R.string.toplist_yesterday)
|
||||
else -> null
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(keyword)) {
|
||||
keyword
|
||||
} else if (MathUtils.hammingWeight(category) == 1) {
|
||||
EhUtils.getCategory(category)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun ListUrlBuilder.toStartArgs(): Bundle {
|
||||
val args = Bundle()
|
||||
args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)
|
||||
@@ -1480,4 +1443,40 @@ class GalleryListScene : SearchBarScene(), OnDragHandlerListener, SearchLayout.H
|
||||
return args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSuitableTitleForUrlBuilder(
|
||||
resources: Resources, urlBuilder: ListUrlBuilder, appName: Boolean
|
||||
): String? {
|
||||
val keyword = urlBuilder.keyword
|
||||
val category = urlBuilder.category
|
||||
val mode = urlBuilder.mode
|
||||
return if (mode == MODE_WHATS_HOT) {
|
||||
resources.getString(R.string.whats_hot)
|
||||
} else if (!keyword.isNullOrEmpty()) {
|
||||
when (mode) {
|
||||
MODE_TOPLIST -> {
|
||||
when (keyword) {
|
||||
"11" -> resources.getString(R.string.toplist_alltime)
|
||||
"12" -> resources.getString(R.string.toplist_pastyear)
|
||||
"13" -> resources.getString(R.string.toplist_pastmonth)
|
||||
"15" -> resources.getString(R.string.toplist_yesterday)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
MODE_TAG -> wrapTagKeyword(keyword)
|
||||
else -> keyword
|
||||
}
|
||||
} else if (category == EhUtils.NONE && urlBuilder.advanceSearch == -1) {
|
||||
when (mode) {
|
||||
MODE_NORMAL -> resources.getString(if (appName) R.string.app_name else R.string.homepage)
|
||||
MODE_SUBSCRIPTION -> resources.getString(R.string.subscription)
|
||||
else -> null
|
||||
}
|
||||
} else if (category.countOneBits() == 1) {
|
||||
EhUtils.getCategory(category)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.databinding.SceneHistoryBinding
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
|
||||
import com.hippo.ehviewer.ui.CommonOperations
|
||||
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
|
||||
@@ -73,7 +74,7 @@ class HistoryScene : BaseToolbarScene() {
|
||||
}
|
||||
})
|
||||
}
|
||||
private val mDownloadManager = EhApplication.downloadManager
|
||||
private val mDownloadManager = DownloadManager
|
||||
private val mDownloadInfoListener: DownloadInfoListener by lazy {
|
||||
object : DownloadInfoListener {
|
||||
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
|
||||
@@ -131,7 +132,7 @@ class HistoryScene : BaseToolbarScene() {
|
||||
val historyData = Pager(
|
||||
PagingConfig(20)
|
||||
) {
|
||||
EhDB.getHistoryLazyList()
|
||||
EhDB.historyLazyList
|
||||
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
val layoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)
|
||||
@@ -296,7 +297,7 @@ class HistoryScene : BaseToolbarScene() {
|
||||
}
|
||||
|
||||
3 -> {
|
||||
val labelRawList = EhApplication.downloadManager.labelList
|
||||
val labelRawList = DownloadManager.labelList
|
||||
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
|
||||
labelList.add(getString(R.string.default_download_label_name))
|
||||
var i = 0
|
||||
@@ -362,7 +363,7 @@ class HistoryScene : BaseToolbarScene() {
|
||||
private val mGi: GalleryInfo
|
||||
) : DialogInterface.OnClickListener {
|
||||
override fun onClick(dialog: DialogInterface, which: Int) {
|
||||
val downloadManager = EhApplication.downloadManager
|
||||
val downloadManager = DownloadManager
|
||||
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
|
||||
val label = if (which == 0) null else mLabels[which]
|
||||
downloadManager.changeLabel(listOf(downloadInfo), label)
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.EhClient
|
||||
import com.hippo.ehviewer.client.EhRequest
|
||||
import com.hippo.util.ExceptionUtils
|
||||
import com.hippo.view.ViewTransition
|
||||
import com.hippo.yorozuya.AssertUtils
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
/**
|
||||
@@ -130,9 +129,7 @@ class ProgressScene : BaseScene(), View.OnClickListener {
|
||||
val view = inflater.inflate(R.layout.scene_progress, container, false)
|
||||
val progress = ViewUtils.`$$`(view, R.id.progress)
|
||||
mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
|
||||
val context = context
|
||||
AssertUtils.assertNotNull(context)
|
||||
val drawable = ContextCompat.getDrawable(context!!, R.drawable.big_sad_pandroid)
|
||||
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_sad_pandroid)
|
||||
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
mTip!!.setCompoundDrawables(null, drawable, null, null)
|
||||
mTip!!.setOnClickListener(this)
|
||||
|
||||
@@ -127,6 +127,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
|
||||
fun setSearchBarText(text: String?) {
|
||||
binding.toolbar.text = text
|
||||
binding.searchview.setText(text)
|
||||
}
|
||||
|
||||
fun setEditTextHint(hint: String?) {
|
||||
@@ -168,18 +169,6 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
mAllowEmptySearch = allowEmptySearch
|
||||
}
|
||||
|
||||
private fun wrapTagKeyword(keyword: String): String {
|
||||
return if (keyword.endsWith(':')) {
|
||||
keyword
|
||||
} else if (keyword.contains(' ')) {
|
||||
val tag = keyword.substringAfter(':')
|
||||
val prefix = keyword.dropLast(tag.length)
|
||||
"$prefix\"$tag$\" "
|
||||
} else {
|
||||
"$keyword$ "
|
||||
}
|
||||
}
|
||||
|
||||
interface SuggestionProvider {
|
||||
|
||||
fun providerSuggestions(text: String): List<Suggestion>?
|
||||
@@ -268,7 +257,7 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
edittext.let {
|
||||
val keywords = it.text.toString().substringBeforeLast(' ', "")
|
||||
val keyword = wrapTagKeyword(mKeyword)
|
||||
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword" else keyword
|
||||
val newKeywords = if (keywords.isNotEmpty()) "$keywords $keyword " else "$keyword "
|
||||
it.setText(newKeywords)
|
||||
it.setSelection(newKeywords.length)
|
||||
}
|
||||
@@ -370,3 +359,15 @@ abstract class SearchBarScene : BaseScene(), ToolBarScene {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun wrapTagKeyword(keyword: String): String {
|
||||
return if (keyword.endsWith(':')) {
|
||||
keyword
|
||||
} else if (keyword.contains(' ')) {
|
||||
val tag = keyword.substringAfter(':')
|
||||
val prefix = keyword.dropLast(tag.length)
|
||||
"$prefix\"$tag$\""
|
||||
} else {
|
||||
"$keyword$"
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ fun HistoryScreen(hostFragment: HistoryComposeScreenFragmentBridge) {
|
||||
Pager(
|
||||
PagingConfig(20)
|
||||
) {
|
||||
EhDB.getHistoryLazyList()
|
||||
EhDB.historyLazyList
|
||||
}.flow
|
||||
}.collectAsLazyPagingItems()
|
||||
|
||||
|
||||
@@ -27,15 +27,15 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||
import java.util.Locale
|
||||
|
||||
private val downloadManager = EhApplication.downloadManager
|
||||
private val downloadManager = DownloadManager
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -25,18 +25,18 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
private val downloadManager = EhApplication.downloadManager
|
||||
private val downloadManager = DownloadManager
|
||||
|
||||
@Composable
|
||||
private fun DialogSelectorItem(
|
||||
|
||||
@@ -10,14 +10,16 @@ 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 = (rating * 2).toInt().coerceAtLeast(0).coerceAtMost(10)
|
||||
val fullStar = r.floorDiv(2)
|
||||
val r = ceil(rating * 2).toInt().coerceIn(0, 10)
|
||||
val fullStar = r / 2
|
||||
val halfStar = r % 2
|
||||
val outlineStar = 5 - fullStar - halfStar
|
||||
val colorYellow800 = Color(0xfff9a825)
|
||||
Row {
|
||||
repeat(fullStar) {
|
||||
Icon(
|
||||
|
||||
@@ -73,9 +73,4 @@ public class FixedThumb extends LoadImageView {
|
||||
|
||||
setScaleType(ScaleType.FIT_CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreSetImageResource(int resId, boolean isTarget) {
|
||||
setScaleType(ScaleType.FIT_CENTER);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.forEach
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -103,8 +104,8 @@ class SearchLayout @JvmOverloads constructor(
|
||||
mChip.isChecked = NumberUtils.int2boolean(mPair.first and mCategoryStored)
|
||||
mChip.setOnLongClickListener {
|
||||
if (mChip.isChecked) {
|
||||
for (i in 0 until mCategoryGroup.childCount) {
|
||||
(mCategoryGroup.getChildAt(i) as Chip).isChecked = true
|
||||
mCategoryGroup.forEach {
|
||||
(it as IdentifiedChip).isChecked = true
|
||||
}
|
||||
mChip.isChecked = false
|
||||
} else {
|
||||
@@ -115,10 +116,10 @@ class SearchLayout @JvmOverloads constructor(
|
||||
}
|
||||
mCategoryGroup.addView(mChip)
|
||||
}
|
||||
mCategoryGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
mCategoryGroup.setOnCheckedStateChangeListener { group, checkedIds ->
|
||||
var mCategory = 0
|
||||
for (index in checkedIds) {
|
||||
mCategory = mCategory or findViewById<IdentifiedChip>(index).idt
|
||||
mCategory = mCategory or group.findViewById<IdentifiedChip>(index).idt
|
||||
}
|
||||
mSharePref.edit { putInt(SEARCH_CATEGORY_PREF, mCategory) }
|
||||
}
|
||||
@@ -160,6 +161,14 @@ class SearchLayout @JvmOverloads constructor(
|
||||
mNormalSearchMode.check(id)
|
||||
}
|
||||
|
||||
fun setCategory(category: Int) {
|
||||
mCategoryGroup.forEach {
|
||||
(it as IdentifiedChip).apply {
|
||||
isChecked = idt and category != 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectImage() {
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onSelectImage()
|
||||
@@ -245,7 +254,7 @@ class SearchLayout @JvmOverloads constructor(
|
||||
post { setSearchMode(tab.position) }
|
||||
}
|
||||
|
||||
fun setSearchMode(@SearchMode mode: Int) {
|
||||
private fun setSearchMode(@SearchMode mode: Int) {
|
||||
val oldItemCount = mAdapter.itemCount
|
||||
mSearchMode = mode
|
||||
val newItemCount = mAdapter.itemCount
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,8 @@ class Image private constructor(private val src: CloseableSource) {
|
||||
(mObtainedDrawable as? AnimatedImageDrawable)?.stop()
|
||||
(mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle()
|
||||
mObtainedDrawable?.callback = null
|
||||
mObtainedDrawable = null
|
||||
(mObtainedDrawable as? AnimatedImageDrawable)?.let { src.close() }
|
||||
mObtainedDrawable = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.yorozuya.thread.PriorityThreadFactory;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class IoThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
|
||||
private final static ThreadPoolExecutor INSTANCE =
|
||||
IoThreadPoolExecutor.newInstance(3, 32, 1L, TimeUnit.SECONDS,
|
||||
new PriorityThreadFactory("IO", android.os.Process.THREAD_PRIORITY_BACKGROUND));
|
||||
|
||||
private IoThreadPoolExecutor(
|
||||
int corePoolSize,
|
||||
int maximumPoolSize,
|
||||
long keepAliveTime,
|
||||
TimeUnit unit,
|
||||
BlockingQueue<Runnable> workQueue,
|
||||
ThreadFactory threadFactory,
|
||||
RejectedExecutionHandler handler
|
||||
) {
|
||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static ThreadPoolExecutor newInstance(
|
||||
int corePoolSize,
|
||||
int maximumPoolSize,
|
||||
long keepAliveTime,
|
||||
TimeUnit unit,
|
||||
ThreadFactory threadFactory
|
||||
) {
|
||||
ThreadQueue queue = new ThreadQueue();
|
||||
PutRunnableBackHandler handler = new PutRunnableBackHandler();
|
||||
IoThreadPoolExecutor executor = new IoThreadPoolExecutor(
|
||||
corePoolSize, maximumPoolSize, keepAliveTime, unit, queue, threadFactory, handler);
|
||||
queue.setThreadPoolExecutor(executor);
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static class ThreadQueue extends LinkedBlockingQueue<Runnable> {
|
||||
|
||||
private ThreadPoolExecutor executor;
|
||||
|
||||
void setThreadPoolExecutor(ThreadPoolExecutor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean offer(@NonNull Runnable o) {
|
||||
int allWorkingThreads = executor.getActiveCount() + super.size();
|
||||
return allWorkingThreads < executor.getPoolSize() && super.offer(o);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PutRunnableBackHandler implements RejectedExecutionHandler {
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
try {
|
||||
executor.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RejectedExecutionException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.util.Log;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public final class PackageUtils {
|
||||
|
||||
private static final String TAG = PackageUtils.class.getSimpleName();
|
||||
|
||||
public static String getSignature(Context context, String packageName) {
|
||||
try {
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
|
||||
Signature[] ss = pi.signatures;
|
||||
if (ss != null && ss.length >= 1) {
|
||||
return computeSHA1(ss[0].toByteArray());
|
||||
} else {
|
||||
Log.e(TAG, "Can't find signature in package " + packageName);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
ExceptionUtils.throwIfFatal(e);
|
||||
Log.e(TAG, "Can't find package " + packageName, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return looks like A1:43:6B:34... or null
|
||||
*/
|
||||
public static String computeSHA1(final byte[] certRaw) {
|
||||
StringBuilder sb = new StringBuilder(59);
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA1");
|
||||
byte[] sha1 = md.digest(certRaw);
|
||||
int length = sha1.length;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i != 0) {
|
||||
sb.append(':');
|
||||
}
|
||||
byte b = sha1[i];
|
||||
String appendStr = Integer.toString(b & 0xff, 16);
|
||||
if (appendStr.length() == 1) {
|
||||
sb.append(0);
|
||||
}
|
||||
sb.append(appendStr);
|
||||
}
|
||||
|
||||
return sb.toString().toUpperCase();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.e(TAG, "Can't final Algorithm SHA1", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,99 +17,50 @@
|
||||
package com.hippo.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.TransitionDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil.imageLoader
|
||||
import coil.request.Disposable
|
||||
import coil.request.ImageRequest
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import coil.load
|
||||
import coil.size.Size
|
||||
import com.hippo.drawable.PreciselyClipDrawable
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
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 mRetryType: Int = 0
|
||||
private var mFailed: Boolean = false
|
||||
private var task: Disposable? = null
|
||||
private val imageDrawable: Drawable?
|
||||
get() {
|
||||
var drawable = drawable
|
||||
if (drawable is TransitionDrawable) {
|
||||
if (drawable.numberOfLayers == 2) {
|
||||
drawable = drawable.getDrawable(1)
|
||||
}
|
||||
}
|
||||
if (drawable is PreciselyClipDrawable) {
|
||||
drawable = drawable.wrappedDrawable
|
||||
}
|
||||
return drawable
|
||||
private var mUrl: String? = null
|
||||
|
||||
@RetryType
|
||||
private val mRetryType: Int =
|
||||
context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0).run {
|
||||
getInt(R.styleable.LoadImageView_retryType, 0).also { recycle() }
|
||||
}
|
||||
|
||||
init {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0)
|
||||
setRetryType(a.getInt(R.styleable.LoadImageView_retryType, 0))
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (mFailed) onFailure()
|
||||
else task?.run { if (isDisposed) reload() }
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
task?.dispose()
|
||||
clearDrawable()
|
||||
}
|
||||
|
||||
private fun clearDrawable() {
|
||||
setImageDrawable(null)
|
||||
}
|
||||
|
||||
private fun clearRetry() {
|
||||
if (mRetryType == RETRY_TYPE_CLICK) {
|
||||
setOnClickListener(null)
|
||||
isClickable = false
|
||||
} else if (mRetryType == RETRY_TYPE_LONG_CLICK) {
|
||||
setOnLongClickListener(null)
|
||||
isLongClickable = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRetryType(@RetryType retryType: Int) {
|
||||
if (mRetryType != retryType) {
|
||||
val oldRetryType = mRetryType
|
||||
mRetryType = retryType
|
||||
|
||||
if (mFailed) {
|
||||
if (oldRetryType == RETRY_TYPE_CLICK) {
|
||||
setOnClickListener(null)
|
||||
isClickable = false
|
||||
} else if (oldRetryType == RETRY_TYPE_LONG_CLICK) {
|
||||
setOnLongClickListener(null)
|
||||
isLongClickable = false
|
||||
}
|
||||
|
||||
if (retryType == RETRY_TYPE_CLICK) {
|
||||
setOnClickListener(this)
|
||||
} else if (retryType == RETRY_TYPE_LONG_CLICK) {
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
private fun setRetry(canRetry: Boolean) {
|
||||
when (mRetryType) {
|
||||
RETRY_TYPE_CLICK -> {
|
||||
setOnClickListener(if (canRetry) this else null)
|
||||
isClickable = canRetry
|
||||
}
|
||||
|
||||
RETRY_TYPE_LONG_CLICK -> {
|
||||
setOnLongClickListener(if (canRetry) this else null)
|
||||
isLongClickable = canRetry
|
||||
}
|
||||
|
||||
RETRY_TYPE_NONE -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,76 +78,48 @@ open class LoadImageView @JvmOverloads constructor(
|
||||
mClipHeight = Integer.MIN_VALUE
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun load(url: String, isLoaded: Boolean = false) {
|
||||
if ((mRequest?.data as? String?) == url)
|
||||
return
|
||||
mFailed = false
|
||||
clearRetry()
|
||||
task?.dispose()
|
||||
mRequest =
|
||||
ImageRequest.Builder(context).data(url).crossfade(false)
|
||||
.size(Size.ORIGINAL).target(
|
||||
{ onWait() },
|
||||
{ onFailure() },
|
||||
{ onGetValue(it, isLoaded) }
|
||||
).build().also { load(it) }
|
||||
}
|
||||
|
||||
fun load(request: ImageRequest) {
|
||||
task = context.imageLoader.enqueue(request)
|
||||
fun load(url: String, crossfade: Boolean = true) {
|
||||
mUrl = url
|
||||
load(url) {
|
||||
size(Size.ORIGINAL)
|
||||
if (!crossfade) crossfade(false)
|
||||
listener(
|
||||
{ setRetry(false) },
|
||||
{
|
||||
MergeInterceptor.onCancel(url)
|
||||
setRetry(true)
|
||||
},
|
||||
{ _, _ ->
|
||||
super.setImageDrawable(errorDrawable)
|
||||
setRetry(true)
|
||||
},
|
||||
{ _, _ -> setRetry(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reload() {
|
||||
mRequest?.let { load(it) }
|
||||
mUrl?.let { this.load(it) }
|
||||
}
|
||||
|
||||
|
||||
private fun onWait() {
|
||||
mFailed = false
|
||||
clearDrawable()
|
||||
override fun setImageDrawable(drawable: Drawable?) {
|
||||
var newDrawable = drawable
|
||||
if (newDrawable != null) {
|
||||
if (Integer.MIN_VALUE != mOffsetX) {
|
||||
newDrawable =
|
||||
PreciselyClipDrawable(newDrawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
|
||||
}
|
||||
onPreSetImageDrawable(newDrawable, true)
|
||||
}
|
||||
super.setImageDrawable(newDrawable)
|
||||
}
|
||||
|
||||
private fun onGetValue(drawable: Drawable, isLoaded: Boolean) {
|
||||
var drawable = drawable
|
||||
clearDrawable()
|
||||
|
||||
if (Integer.MIN_VALUE != mOffsetX) {
|
||||
drawable = PreciselyClipDrawable(drawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
|
||||
}
|
||||
|
||||
onPreSetImageDrawable(drawable, true)
|
||||
if (isShown && !isLoaded) {
|
||||
val layers = arrayOfNulls<Drawable>(2)
|
||||
layers[0] = ColorDrawable(Color.TRANSPARENT)
|
||||
layers[1] = drawable
|
||||
val transitionDrawable = TransitionDrawable(layers)
|
||||
setImageDrawable(transitionDrawable)
|
||||
transitionDrawable.startTransition(300)
|
||||
} else {
|
||||
setImageDrawable(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFailure() {
|
||||
mFailed = true
|
||||
clearDrawable()
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.image_failed)
|
||||
onPreSetImageDrawable(drawable, true)
|
||||
setImageDrawable(drawable)
|
||||
when (mRetryType) {
|
||||
RETRY_TYPE_CLICK -> {
|
||||
setOnClickListener(this)
|
||||
}
|
||||
|
||||
RETRY_TYPE_LONG_CLICK -> {
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
else -> {
|
||||
mRequest = null
|
||||
}
|
||||
override fun getDrawable(): Drawable? {
|
||||
var newDrawable = super.getDrawable()
|
||||
if (newDrawable is PreciselyClipDrawable) {
|
||||
newDrawable = newDrawable.drawable
|
||||
}
|
||||
return newDrawable
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -210,8 +133,6 @@ open class LoadImageView @JvmOverloads constructor(
|
||||
|
||||
open fun onPreSetImageDrawable(drawable: Drawable?, isTarget: Boolean) {}
|
||||
|
||||
open fun onPreSetImageResource(resId: Int, isTarget: Boolean) {}
|
||||
|
||||
@IntDef(RETRY_TYPE_NONE, RETRY_TYPE_CLICK, RETRY_TYPE_LONG_CLICK)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class RetryType
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.yorozuya;
|
||||
|
||||
public class AssertError extends Error {
|
||||
|
||||
public AssertError(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.yorozuya;
|
||||
|
||||
public class AssertException extends Exception {
|
||||
|
||||
public AssertException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.yorozuya;
|
||||
|
||||
public final class AssertUtils {
|
||||
private AssertUtils() {
|
||||
}
|
||||
|
||||
public static void assertTrue(boolean cond) {
|
||||
assertTrue("Condition has to be true.", cond);
|
||||
}
|
||||
|
||||
public static void assertTrueEx(boolean cond) throws AssertException {
|
||||
assertTrueEx("Condition has to be true.", cond);
|
||||
}
|
||||
|
||||
public static void assertTrue(String message, boolean cond) {
|
||||
if (!cond) {
|
||||
throw new AssertError(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertTrueEx(String message, boolean cond) throws AssertException {
|
||||
if (!cond) {
|
||||
throw new AssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNull(Object object) {
|
||||
assertNull("Should be null", object);
|
||||
}
|
||||
|
||||
public static void assertNullEx(Object object) throws AssertException {
|
||||
assertNullEx("Should be null", object);
|
||||
}
|
||||
|
||||
public static void assertNull(String message, Object object) {
|
||||
if (object != null) {
|
||||
throw new AssertError(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNullEx(String message, Object object) throws AssertException {
|
||||
if (object != null) {
|
||||
throw new AssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNotNull(Object object) {
|
||||
assertNotNull("Should not be null", object);
|
||||
}
|
||||
|
||||
public static void assertNotNullEx(Object object) throws AssertException {
|
||||
assertNotNullEx("Should not be null", object);
|
||||
}
|
||||
|
||||
public static void assertNotNull(String message, Object object) {
|
||||
if (object == null) {
|
||||
throw new AssertError(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertNotNullEx(String message, Object object) throws AssertException {
|
||||
if (object == null) {
|
||||
throw new AssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertEquals(int expected, int actual) {
|
||||
assertEquals("Should be " + expected + ", but it is " + actual, expected, actual);
|
||||
}
|
||||
|
||||
public static void assertEqualsEx(int expected, int actual) throws AssertException {
|
||||
assertEqualsEx("Should be " + expected + ", but it is " + actual, expected, actual);
|
||||
}
|
||||
|
||||
public static void assertEquals(String message, int expected, int actual) {
|
||||
if (expected != actual) {
|
||||
throw new AssertError(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertEqualsEx(String message, int expected, int actual) throws AssertException {
|
||||
if (expected != actual) {
|
||||
throw new AssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertInstanceOf(Object obj, Class clazz) {
|
||||
assertInstanceOf("The object should be instance of " + clazz.getName(), obj, clazz);
|
||||
}
|
||||
|
||||
public static void assertInstanceOfEx(Object obj, Class clazz) throws AssertException {
|
||||
assertInstanceOfEx("The object should be instance of " + clazz.getName(), obj, clazz);
|
||||
}
|
||||
|
||||
public static void assertInstanceOf(String message, Object obj, Class clazz) {
|
||||
if (!clazz.isInstance(obj)) {
|
||||
throw new AssertError(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertInstanceOfEx(String message, Object obj, Class clazz) throws AssertException {
|
||||
if (!clazz.isInstance(obj)) {
|
||||
throw new AssertException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.hippo.yorozuya
|
||||
|
||||
fun assertEquals(expected: Int, actual: Int) {
|
||||
check(expected == actual)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hippo.yorozuya
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
|
||||
fun ResponseBody.copyToFile(file: File) {
|
||||
val os = file.outputStream()
|
||||
val channel = os.channel
|
||||
val source = source()
|
||||
try {
|
||||
channel.transferFrom(source, 0, Long.MAX_VALUE)
|
||||
} catch (e: Throwable) {
|
||||
throw e
|
||||
} finally {
|
||||
source.close()
|
||||
channel.close()
|
||||
os.close()
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package com.hippo.yorozuya;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.jsoup.parser.Parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -30,25 +32,6 @@ public final class StringUtils {
|
||||
'\u00A0', // NO-BREAK SPACE
|
||||
'\u3000', // IDEOGRAPHIC SPACE
|
||||
};
|
||||
private static final String[] ESCAPE_CHARATER_LIST = {
|
||||
"&",
|
||||
"<",
|
||||
">",
|
||||
""",
|
||||
"'",
|
||||
"×",
|
||||
" "
|
||||
};
|
||||
|
||||
private static final String[] UNESCAPE_CHARATER_LIST = {
|
||||
"&",
|
||||
"<",
|
||||
">",
|
||||
"\"",
|
||||
"'",
|
||||
"×",
|
||||
" "
|
||||
};
|
||||
|
||||
private StringUtils() {
|
||||
}
|
||||
@@ -57,7 +40,7 @@ public final class StringUtils {
|
||||
* Unescape xml. It do not work perfectly.
|
||||
*/
|
||||
public static String unescapeXml(String str) {
|
||||
return replaceEach(str, ESCAPE_CHARATER_LIST, UNESCAPE_CHARATER_LIST);
|
||||
return Parser.unescapeEntities(str, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,218 +124,6 @@ public final class StringUtils {
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Replaces all occurrences of Strings within another String.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* A {@code null} reference passed to this method is a no-op, or if
|
||||
* any "search string" or "string to replace" is null, that replace will be
|
||||
* ignored. This will not repeat. For repeating replaces, call the
|
||||
* overloaded method.
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* StringUtils.replaceEach(null, *, *) = null
|
||||
* StringUtils.replaceEach("", *, *) = ""
|
||||
* StringUtils.replaceEach("aba", null, null) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[0], null) = "aba"
|
||||
* StringUtils.replaceEach("aba", null, new String[0]) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[]{"a"}, null) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}) = "b"
|
||||
* StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}) = "aba"
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}) = "wcte"
|
||||
* (example of how it does not repeat)
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}) = "dcte"
|
||||
* </pre>
|
||||
*
|
||||
* @param text text to search and replace in, no-op if null
|
||||
* @param searchList the Strings to search for, no-op if null
|
||||
* @param replacementList the Strings to replace them with, no-op if null
|
||||
* @return the text with any replacements processed, {@code null} if
|
||||
* null String input
|
||||
* @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok,
|
||||
* and/or size 0)
|
||||
*/
|
||||
// Get from org.apache.commons.lang3.StringUtils
|
||||
public static String replaceEach(final String text, final String[] searchList,
|
||||
final String[] replacementList) {
|
||||
return replaceEach(text, searchList, replacementList, false, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Replaces all occurrences of Strings within another String.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* A {@code null} reference passed to this method is a no-op, or if
|
||||
* any "search string" or "string to replace" is null, that replace will be
|
||||
* ignored.
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* StringUtils.replaceEach(null, *, *, *) = null
|
||||
* StringUtils.replaceEach("", *, *, *) = ""
|
||||
* StringUtils.replaceEach("aba", null, null, *) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[0], null, *) = "aba"
|
||||
* StringUtils.replaceEach("aba", null, new String[0], *) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
|
||||
* StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
|
||||
* StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
|
||||
* (example of how it repeats)
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
|
||||
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalStateException
|
||||
* </pre>
|
||||
*
|
||||
* @param text text to search and replace in, no-op if null
|
||||
* @param searchList the Strings to search for, no-op if null
|
||||
* @param replacementList the Strings to replace them with, no-op if null
|
||||
* @param repeat if true, then replace repeatedly
|
||||
* until there are no more possible replacements or timeToLive < 0
|
||||
* @param timeToLive if less than 0 then there is a circular reference and endless
|
||||
* loop
|
||||
* @return the text with any replacements processed, {@code null} if
|
||||
* null String input
|
||||
* @throws IllegalStateException if the search is repeating and there is an endless loop due
|
||||
* to outputs of one being inputs to another
|
||||
* @throws IllegalArgumentException if the lengths of the arrays are not the same (null is ok,
|
||||
* and/or size 0)
|
||||
*/
|
||||
// Get from org.apache.commons.lang3.StringUtils
|
||||
private static String replaceEach(
|
||||
final String text, final String[] searchList,
|
||||
final String[] replacementList, final boolean repeat,
|
||||
final int timeToLive) {
|
||||
|
||||
// mchyzer Performance note: This creates very few new objects (one major goal)
|
||||
// let me know if there are performance requests, we can create a harness to measure
|
||||
|
||||
if (text == null || TextUtils.isEmpty(text) || searchList == null ||
|
||||
searchList.length == 0 || replacementList == null || replacementList.length == 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// if recursing, this shouldn't be less than 0
|
||||
if (timeToLive < 0) {
|
||||
throw new IllegalStateException("Aborting to protect against StackOverflowError - " +
|
||||
"output of one loop is the input of another");
|
||||
}
|
||||
|
||||
final int searchLength = searchList.length;
|
||||
final int replacementLength = replacementList.length;
|
||||
|
||||
// make sure lengths are ok, these need to be equal
|
||||
if (searchLength != replacementLength) {
|
||||
throw new IllegalArgumentException("Search and Replace array lengths don't match: "
|
||||
+ searchLength
|
||||
+ " vs "
|
||||
+ replacementLength);
|
||||
}
|
||||
|
||||
// keep track of which still have matches
|
||||
final boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];
|
||||
|
||||
// index on index that the match was found
|
||||
int textIndex = -1;
|
||||
int replaceIndex = -1;
|
||||
int tempIndex = -1;
|
||||
|
||||
// index of replace array that will replace the search string found
|
||||
// NOTE: logic duplicated below START
|
||||
for (int i = 0; i < searchLength; i++) {
|
||||
if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
|
||||
TextUtils.isEmpty(searchList[i]) || replacementList[i] == null) {
|
||||
continue;
|
||||
}
|
||||
tempIndex = text.indexOf(searchList[i]);
|
||||
|
||||
// see if we need to keep searching for this
|
||||
if (tempIndex == -1) {
|
||||
noMoreMatchesForReplIndex[i] = true;
|
||||
} else {
|
||||
if (textIndex == -1 || tempIndex < textIndex) {
|
||||
textIndex = tempIndex;
|
||||
replaceIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: logic mostly below END
|
||||
|
||||
// no search strings found, we are done
|
||||
if (textIndex == -1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
|
||||
// get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit
|
||||
int increase = 0;
|
||||
|
||||
// count the replacement text elements that are larger than their corresponding text being replaced
|
||||
for (int i = 0; i < searchList.length; i++) {
|
||||
if (searchList[i] == null || replacementList[i] == null) {
|
||||
continue;
|
||||
}
|
||||
final int greater = replacementList[i].length() - searchList[i].length();
|
||||
if (greater > 0) {
|
||||
increase += 3 * greater; // assume 3 matches
|
||||
}
|
||||
}
|
||||
// have upper-bound at 20% increase, then let Java take over
|
||||
increase = Math.min(increase, text.length() / 5);
|
||||
|
||||
final StringBuilder buf = new StringBuilder(text.length() + increase);
|
||||
|
||||
while (textIndex != -1) {
|
||||
|
||||
for (int i = start; i < textIndex; i++) {
|
||||
buf.append(text.charAt(i));
|
||||
}
|
||||
buf.append(replacementList[replaceIndex]);
|
||||
|
||||
start = textIndex + searchList[replaceIndex].length();
|
||||
|
||||
textIndex = -1;
|
||||
replaceIndex = -1;
|
||||
tempIndex = -1;
|
||||
// find the next earliest match
|
||||
// NOTE: logic mostly duplicated above START
|
||||
for (int i = 0; i < searchLength; i++) {
|
||||
if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
|
||||
TextUtils.isEmpty(searchList[i]) || replacementList[i] == null) {
|
||||
continue;
|
||||
}
|
||||
tempIndex = text.indexOf(searchList[i], start);
|
||||
|
||||
// see if we need to keep searching for this
|
||||
if (tempIndex == -1) {
|
||||
noMoreMatchesForReplIndex[i] = true;
|
||||
} else {
|
||||
if (textIndex == -1 || tempIndex < textIndex) {
|
||||
textIndex = tempIndex;
|
||||
replaceIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: logic duplicated above END
|
||||
|
||||
}
|
||||
final int textLength = text.length();
|
||||
for (int i = start; i < textLength; i++) {
|
||||
buf.append(text.charAt(i));
|
||||
}
|
||||
final String result = buf.toString();
|
||||
if (!repeat) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1);
|
||||
}
|
||||
|
||||
public static boolean endsWith(String string, String[] suffixs) {
|
||||
for (int i = 0, n = suffixs.length; i < n; i++) {
|
||||
if (string.endsWith(suffixs[i])) {
|
||||
@@ -464,38 +235,6 @@ public final class StringUtils {
|
||||
return null == str ? 0 : str.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Counts how many times the char appears in the given string.</p>
|
||||
*
|
||||
* <p>A {@code null} or empty ("") String input returns {@code 0}.</p>
|
||||
*
|
||||
* <pre>
|
||||
* StringUtils.countMatches(null, *) = 0
|
||||
* StringUtils.countMatches("", *) = 0
|
||||
* StringUtils.countMatches("abba", 0) = 0
|
||||
* StringUtils.countMatches("abba", 'a') = 2
|
||||
* StringUtils.countMatches("abba", 'b') = 2
|
||||
* StringUtils.countMatches("abba", 'x') = 0
|
||||
* </pre>
|
||||
*
|
||||
* @param str the CharSequence to check, may be null
|
||||
* @param ch the char to count
|
||||
* @return the number of occurrences, 0 if the CharSequence is {@code null}
|
||||
*/
|
||||
public static int countMatches(final CharSequence str, final char ch) {
|
||||
if (TextUtils.isEmpty(str)) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
// We could also call str.toCharArray() for faster look ups but that would generate more garbage.
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
if (ch == str.charAt(i)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* All null or empty, or all not
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.hippo.yorozuya
|
||||
|
||||
import org.jsoup.parser.Parser
|
||||
|
||||
fun String.unescapeXml(): String {
|
||||
return StringUtils.unescapeXml(this)
|
||||
return Parser.unescapeEntities(this, true)
|
||||
}
|
||||
|
||||
inline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T {
|
||||
return block(trim())
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.yorozuya.thread;
|
||||
|
||||
public class PVLock {
|
||||
|
||||
private int mCounter;
|
||||
|
||||
public PVLock(int count) {
|
||||
mCounter = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain
|
||||
*/
|
||||
public synchronized void p() throws InterruptedException {
|
||||
while (true) {
|
||||
if (mCounter > 0) {
|
||||
mCounter--;
|
||||
break;
|
||||
} else {
|
||||
this.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release
|
||||
*/
|
||||
public synchronized void v() {
|
||||
mCounter++;
|
||||
if (mCounter > 0) {
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.yorozuya.thread;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
public class PriorityThread extends Thread {
|
||||
|
||||
private final int mPriority;
|
||||
|
||||
public PriorityThread(Runnable runnable, int priority) {
|
||||
super(runnable);
|
||||
mPriority = priority;
|
||||
}
|
||||
|
||||
public PriorityThread(Runnable runnable, String name, int priority) {
|
||||
super(runnable, name);
|
||||
mPriority = priority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Process.setThreadPriority(mPriority);
|
||||
super.run();
|
||||
}
|
||||
}
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user