Compare commits

...
71 Commits
Author SHA1 Message Date
txyyh 309e2a715e Release 1.8.4.0-alpha01
Releases / Build (push) Has been cancelled
2022-12-07 12:28:52 +00:00
tarsin fbb4afa4f7 Fix searchlayout style 2022-12-07 20:26:54 +08:00
tarsin 90863b95f4 Fix fab position 2022-12-07 20:18:56 +08:00
tarsin 3576eaf3ff Try to fix thumb load 2022-12-07 20:08:53 +08:00
tarsin f14c9741ea Increase card margin 2022-12-07 19:47:49 +08:00
tarsin 6d04e51e10 Fix amoled mode apply 2022-12-07 19:26:17 +08:00
tarsin 6ea1aa6c1a Fix searchbar 2022-12-07 18:51:44 +08:00
tarsin c86e1a9409 Drop unused widget 2022-12-07 18:00:50 +08:00
tarsin f1845f2d4e Use transparent card background 2022-12-07 14:41:59 +08:00
tarsin a20ca4ea2e Convert FavouriteScene to New SearchBar, Kill hippo's searchbar 2022-12-07 14:37:32 +08:00
tarsin 0effc0d181 Fix searchfab 2022-12-07 14:18:03 +08:00
tarsin 7ce273ec67 Initial migrate GalleryListScene to New SearchBar, #397 2022-12-07 13:47:10 +08:00
tarsin 39e080cdca Fix thumb flicker 2022-12-06 18:14:17 +08:00
tarsin 17584ae5fb Avoid double authenticating on Android 10 2022-12-06 12:01:58 +08:00
小白-白 c46c72d66d Add private category 2022-12-05 18:05:16 +08:00
小白-白 36f00ab568 Fix disowned parsing 2022-12-05 18:05:16 +08:00
tarsin dae7dacf3d Rework SimilarityScan 2022-12-05 11:43:52 +08:00
tarsin b69b72cd1b Resolve #489 2022-12-05 09:57:42 +08:00
tarsin 68d1e39410 Refactor dependency with lazykt 2022-12-04 22:31:02 +08:00
tarsin d4482dec23 Rename .java to .kt 2022-12-04 22:31:02 +08:00
tarsin 8f36cecc7a Cleanup Code 2022-12-04 18:23:15 +08:00
tarsin a59fbcc9c8 Thanks coil 2022-12-04 12:47:14 +08:00
tarsin 25cddb2d61 Migrate to coil imageloader, Drop Conaco and BeerBelly as well
TODO: Fix SimilarityScan
2022-12-04 12:43:44 +08:00
tarsin 9a7af235b8 Fix gif render, Resolve #485 2022-12-03 09:26:08 +08:00
FooIbar c9751ab2a2 Fix simpleTags parsing (#484)
* Fix simpleTags parsing

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

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2022-11-25 09:54:24 +08:00
小白-白 949f3cb246 Fix jump on tag and uploader page 2022-11-24 12:03:26 +08:00
FooIbar 501855a8c0 Replace "..." with ellipsis character 2022-11-23 00:07:36 +08:00
FooIbar 44634be13d Remove untranslatable string 2022-11-23 00:07:36 +08:00
FooIbar 7c718df1af Use app:showAsAction with appcompat 2022-11-23 00:07:36 +08:00
FooIbar 16cca9cbbd Remove redundant namespace 2022-11-23 00:07:36 +08:00
FooIbar b715320298 android:forceDarkAllowed requires API 29 2022-11-23 00:07:36 +08:00
FooIbar d31d9eb137 WRITE_EXTERNAL_STORAGE is not required on API 29 2022-11-23 00:07:36 +08:00
tarsin b53c29c035 Fix fab position 2022-11-22 21:26:13 +08:00
tarsin d3bf20bee6 Refresh tokens, fuck github 2022-11-22 19:57:00 +08:00
tarsin cff445ddf5 Use latest mdc for searchbar migration 2022-11-22 19:47:30 +08:00
143 changed files with 1693 additions and 6264 deletions

No files matched your search

+1
View File
@@ -78,6 +78,7 @@ with Material Design 3 and Dynamic Color Support
Here is the libraries
- [AOSP & AndroidX](http://source.android.com/)
- [Coil](https://coil-kt.github.io/coil/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [GnuTLS](https://gnutls.org/)
- [material-components-android](https://github.com/material-components/material-components-android)
+13 -6
View File
@@ -6,7 +6,7 @@ plugins {
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
buildToolsVersion "33.0.1"
ndkVersion "25.1.8937393"
splits {
@@ -31,8 +31,8 @@ android {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 28
targetSdkVersion 33
versionCode 180023
versionName "1.8.3.2"
versionCode 180025
versionName "1.8.4.0-alpha01"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
@@ -50,6 +50,12 @@ android {
kotlinOptions {
jvmTarget = '19'
freeCompilerArgs += [
// https://kotlinlang.org/docs/compiler-reference.html#progressive
"-progressive",
// Generate smaller bytecode by not generating runtime not-null assertions.
"-Xno-call-assertions",
"-Xno-param-assertions",
"-Xno-receiver-assertions",
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
]
@@ -64,6 +70,7 @@ android {
packagingOptions {
resources {
excludes += "/META-INF/**"
excludes += "/okhttp3/**"
excludes += "/kotlin/**"
excludes += "**.txt"
excludes += "**.bin"
@@ -86,6 +93,7 @@ android {
}
buildFeatures {
buildConfig true
compose true
}
@@ -173,20 +181,19 @@ dependencies {
implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"
implementation 'androidx.viewpager:viewpager:1.1.0-alpha01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.5.0'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.7.0'
implementation 'com.google.android.material:material:1.8.0-SNAPSHOT'
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'io.coil-kt:coil:2.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jsoup:jsoup:1.15.3'
}
-4
View File
@@ -14,10 +14,6 @@
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep,allowoptimization class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
+1 -1
View File
@@ -30,7 +30,7 @@
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="28" />
<uses-feature
android:glEsVersion="0x00020000"
+6 -1
View File
@@ -52,6 +52,8 @@ static char *passwd = NULL;
static void *archiveAddr = NULL;
static size_t archiveSize = 0;
#define POPULATE_THRESHOLD (512 * 1024 * 1024) // 512 MiB
const char supportExt[9][6] = {
"jpeg",
"jpg",
@@ -199,7 +201,10 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd
EH_UNUSED(env);
EH_UNUSED(thiz);
archive_ctx *ctx = NULL;
archiveAddr = mmap64(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
int mmap_flags = MAP_PRIVATE;
if (size <= POPULATE_THRESHOLD)
mmap_flags |= MAP_POPULATE;
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
return 0;
-1
View File
@@ -20,7 +20,6 @@ package com.hippo
import java.io.FileDescriptor
object Native {
@JvmStatic
fun initialize() {
System.loadLibrary("ehviewer")
}
@@ -1,28 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
public @interface Implemented {
Class value();
}
@@ -1,23 +1,8 @@
package com.hippo.app
import android.content.Context
import android.os.Build
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
open class BaseDialogBuilder constructor(
context: Context
) : MaterialAlertDialogBuilder(context) {
override fun create(): AlertDialog {
return super.create().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window?.run {
addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes.blurBehindRadius = 32
}
}
}
}
}
) : MaterialAlertDialogBuilder(context)
@@ -1,493 +0,0 @@
/*
* Copyright (C) 2015-2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.beerbelly;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public abstract class BeerBelly<V> {
private static final String TAG = BeerBelly.class.getSimpleName();
private final boolean mHasMemoryCache;
private final boolean mHasDiskCache;
@Nullable
private MemoryCache<V> mMemoryCache;
@Nullable
private DiskCache<V> mDiskCache;
public BeerBelly(BeerBellyParams params) {
params.isValid();
mHasMemoryCache = params.hasMemoryCache;
mHasDiskCache = params.hasDiskCache;
if (mHasMemoryCache) {
initMemoryCache(params.memoryCacheMaxSize);
}
if (mHasDiskCache) {
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
}
}
private void initMemoryCache(int maxSize) {
mMemoryCache = new MemoryCache<>(maxSize, this);
}
private void initDiskCache(File cacheDir, int maxSize) {
// Set up disk cache
try {
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
} catch (IOException e) {
Log.e(TAG, "Can't create disk cache", e);
}
}
protected abstract int sizeOf(String key, V value);
protected abstract void memoryEntryAdded(V value);
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
protected abstract V read(@NonNull InputStreamPipe isPipe);
protected abstract boolean write(OutputStream os, V value);
@Nullable
public LruCache<String, V> getMemoryCache() {
return mMemoryCache;
}
@Nullable
public SimpleDiskCache getDiskCache() {
if (mDiskCache != null) {
return mDiskCache.mDiskCache;
} else {
return null;
}
}
/**
* Check if have memory cache
*
* @return true if have memory cache
*/
public boolean hasMemoryCache() {
return mHasMemoryCache;
}
/**
* Check if have disk cache
*
* @return true if have disk cache
*/
public boolean hasDiskCache() {
return mHasDiskCache;
}
/**
* Get memory cache usage size
*
* @return {@code -1} for no memory cache
*/
public int memorySize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.size();
} else {
return -1;
}
}
/**
* Get memory cache max size
*
* @return {@code -1} for no memory cache
*/
public int memoryMaxSize() {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.maxSize();
} else {
return -1;
}
}
/**
* Get disk cache usage size
*
* @return {@code -1L} for no memory cache
*/
public long diskSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.size();
} else {
return -1L;
}
}
/**
* Get disk cache max size
*
* @return {@code -1L} for no memory cache
*/
public long diskMaxSize() {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.getMaxSize();
} else {
return -1L;
}
}
/**
* Get value from memory cache
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache
*/
public V getFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
return mMemoryCache.get(key);
} else {
return null;
}
}
/**
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
*
* @param key the key to get value
* @return the value you get, null for miss or no memory cache or get error
*/
public V getFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.get(key);
} else {
return null;
}
}
/**
* Get value from memory cache and disk cache. If miss in memory cache and
* get in disk cache, it will put value from disk cache to memory cache.
*
* @param key the key to get value
* @return the value you get
*/
public V get(@NonNull String key) {
V value = getFromMemory(key);
if (value != null) {
// Get it in memory cache
return value;
}
value = getFromDisk(key);
if (value != null) {
// Get it in disk cache
putToMemory(key, value);
return value;
}
return null;
}
/**
* Put value to memory cache
*
* @param key the key
* @param value the value
* @return false if no memory cache
*/
public boolean putToMemory(@NonNull String key, @NonNull V value) {
if (mHasMemoryCache && mMemoryCache != null) {
memoryEntryAdded(value);
mMemoryCache.put(key, value);
return true;
} else {
return false;
}
}
/**
* Put value to disk cache
*
* @param key the key
* @param value the value
* @return false if no disk cache or get error
*/
public boolean putToDisk(@NonNull String key, @NonNull V value) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.put(key, value);
} else {
return false;
}
}
/**
* Put value to memory cache and disk cache
*
* @param key the key
* @param value the value
*/
public void put(@NonNull String key, @NonNull V value) {
putToMemory(key, value);
putToDisk(key, value);
}
public void removeFromMemory(@NonNull String key) {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.remove(key);
}
}
public void removeFromDisk(@NonNull String key) {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.remove(key);
}
}
public void remove(@NonNull String key) {
removeFromMemory(key);
removeFromDisk(key);
}
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.pull(key, os);
} else {
return false;
}
}
/**
* @param key the key
* @param is the input stream to store
* @return false if no disk cache or get error
*/
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
if (mHasDiskCache && mDiskCache != null) {
return mDiskCache.putRaw(key, is);
} else {
return false;
}
}
/**
* Evicts all of the items from the memory cache
*/
public void clearMemory() {
if (mHasMemoryCache && mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
/**
* Clear disk cache
*/
public void clearDisk() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.clear();
}
}
public void flush() {
if (mHasDiskCache && mDiskCache != null) {
mDiskCache.flush();
}
}
/**
* Evicts all of the items from the memory cache and lets the system know
* now would be a good time to garbage collect
*/
public void clear() {
clearMemory();
clearDisk();
}
/**
* A holder class that contains cache parameters.
*/
public static class BeerBellyParams {
/**
* is memory cache available
*/
public boolean hasMemoryCache = false;
/**
* the maximum number of bytes the memory cache should use to store
*/
public int memoryCacheMaxSize = 0;
/**
* is disk cache available
*/
public boolean hasDiskCache = false;
/**
* the dir to store disk cache
*/
public File diskCacheDir = null;
/**
* the maximum number of bytes the disk cache should use to store
*/
public int diskCacheMaxSize = 0;
/**
* Check BeerBellyParams is valid
*/
public void isValid() throws IllegalStateException {
if (!hasMemoryCache && !hasDiskCache) {
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
}
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
}
if (hasDiskCache) {
if (diskCacheDir == null) {
throw new IllegalStateException("Disk cache dir can't be null.");
}
if (diskCacheMaxSize <= 0) {
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
}
}
}
}
private static class DiskCache<E> {
private static final int IO_BUFFER_SIZE = 8 * 1024;
private final SimpleDiskCache mDiskCache;
private final BeerBelly<E> mParent;
private final File mCacheDir;
private final int mMaxSize;
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
mDiskCache = new SimpleDiskCache(cacheDir, size);
mParent = parent;
mCacheDir = cacheDir;
mMaxSize = size;
}
public File getCacheDir() {
return mCacheDir;
}
public int getMaxSize() {
return mMaxSize;
}
public long size() {
return mDiskCache.size();
}
public void flush() {
mDiskCache.flush();
}
public void clear() {
mDiskCache.clear();
}
public E get(String key) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return null;
} else {
return mParent.read(isPipe);
}
}
public boolean put(String key, E value) {
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
BufferedOutputStream buffOut = null;
try {
osPipe.obtain();
OutputStream os = osPipe.open();
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
return mParent.write(os, value);
} catch (IOException e) {
return false;
} finally {
Util.closeQuietly(buffOut);
osPipe.close();
osPipe.release();
}
}
public boolean putRaw(String key, InputStream is) {
return mDiskCache.put(key, is);
}
public void remove(String key) {
mDiskCache.remove(key);
}
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
if (isPipe == null) {
return false;
} else {
try {
isPipe.obtain();
InputStream is = isPipe.open();
Util.copy(is, os);
return true;
} catch (IOException e) {
return false;
} finally {
isPipe.close();
isPipe.release();
}
}
}
}
private class MemoryCache<E> extends LruCache<String, E> {
public BeerBelly<E> mParent;
public MemoryCache(int maxSize, BeerBelly<E> parent) {
super(maxSize);
mParent = parent;
}
@Override
protected int sizeOf(String key, E value) {
return mParent.sizeOf(key, value);
}
@Override
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
}
}
}
@@ -246,6 +246,10 @@ public final class DiskLruCache implements Closeable {
if (!from.renameTo(to)) {
throw new IOException();
}
}
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
@@ -262,10 +266,6 @@ public final class DiskLruCache implements Closeable {
}
};
private static String inputStreamToString(InputStream in) throws IOException {
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
@@ -1,223 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import android.os.Process;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.UiThread;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.yorozuya.thread.PriorityThreadFactory;
import com.hippo.yorozuya.thread.SerialThreadExecutor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
@SuppressWarnings("rawtypes")
public final class Conaco<V> {
public static final int SOURCE_MEMORY = 0;
public static final int SOURCE_DISK = 1;
public static final int SOURCE_NETWORK = 2;
private static final String TAG = Conaco.class.getSimpleName();
private final ValueHelper<V> mHelper;
private final ValueCache<V> mCache;
private final OkHttpClient mOkHttpClient;
private final Register<V> mRegister;
private final SerialThreadExecutor mDiskExecutor;
private final ThreadPoolExecutor mNetworkExecutor;
private final AtomicInteger mIdGenerator;
private final boolean mDebug;
private Conaco(Builder<V> builder) {
mHelper = builder.objectHelper;
BeerBelly.BeerBellyParams beerBellyParams = new BeerBelly.BeerBellyParams();
beerBellyParams.hasMemoryCache = builder.hasMemoryCache;
beerBellyParams.memoryCacheMaxSize = builder.memoryCacheMaxSize;
beerBellyParams.hasDiskCache = builder.hasDiskCache;
beerBellyParams.diskCacheDir = builder.diskCacheDir;
beerBellyParams.diskCacheMaxSize = builder.diskCacheMaxSize;
mCache = new ValueCache<>(beerBellyParams, mHelper);
mOkHttpClient = builder.okHttpClient;
mRegister = new Register<>();
mDiskExecutor = new SerialThreadExecutor(3000L, new LinkedList<>(),
new PriorityThreadFactory("Conaco-Disk", Process.THREAD_PRIORITY_BACKGROUND));
mNetworkExecutor = new ThreadPoolExecutor(3, 3, 5L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new PriorityThreadFactory("Conaco-Network", Process.THREAD_PRIORITY_BACKGROUND));
mIdGenerator = new AtomicInteger();
mDebug = builder.debug;
}
public BeerBelly getBeerBelly() {
return mCache;
}
private void onUnregisterConacoTask(ConacoTask<V> task) {
ConacoTask<V> next = mRegister.getByKey(task.getKey());
if (next != null) {
startConacoTask(next);
}
}
private void startConacoTask(ConacoTask<V> task) {
Unikery<V> unikery = task.getUnikery();
// Can't unikery is gone, finish the task now
if (unikery == null) {
finishConacoTask(task);
return;
}
String key = task.getKey();
V value = null;
// Get from memory
if (key != null && task.useMemoryCache() && mHelper.useMemoryCache(key, null)) {
value = mCache.getFromMemory(key);
}
if (value == null || !unikery.onGetValue(value, SOURCE_MEMORY)) {
unikery.onMiss(SOURCE_MEMORY);
task.start();
} else {
// Get the object, finish the task
finishConacoTask(task);
}
}
@UiThread
public void load(ConacoTask.Builder<V> builder) {
builder.isValid();
if (mDebug) {
Log.d(TAG, "Key " + builder.getKey());
Log.d(TAG, "Url " + builder.getUrl());
}
Unikery<V> unikery = builder.getUnikery();
// Cancel first
cancel(unikery);
// Build conaco task
int id;
//noinspection StatementWithEmptyBody
while ((id = mIdGenerator.getAndIncrement()) == Unikery.INVALID_ID) ;
unikery.setTaskId(id);
builder.setId(id)
.setHelper(mHelper)
.setCache(mCache)
.setOkHttpClient(mOkHttpClient)
.setDiskExecutor(mDiskExecutor)
.setNetworkExecutor(mNetworkExecutor)
.setConaco(this);
ConacoTask<V> task = builder.build();
if (!mRegister.register(id, task)) {
startConacoTask(task);
} else {
// The key is repeated, wait
unikery.onWait();
}
}
@UiThread
public void cancel(Unikery<V> unikery) {
int id = unikery.getTaskId();
if (id != Unikery.INVALID_ID) {
unikery.setTaskId(Unikery.INVALID_ID);
ConacoTask<V> task = mRegister.unregister(id);
if (task != null) {
task.stop();
// Don't need unikery anymore
task.clearUnikery();
// Callback
onUnregisterConacoTask(task);
} else {
Log.e(TAG, "Can't find conaco task by id " + id);
}
}
}
public boolean isLoading(Unikery<V> unikery) {
int id = unikery.getTaskId();
return id != Unikery.INVALID_ID && mRegister.contain(id);
}
/**
* Unregister task, reset unikery id, clear unikery in task, call next
*/
void finishConacoTask(ConacoTask<V> task) {
mRegister.unregister(task.getId());
Unikery unikery = task.getUnikery();
if (unikery != null) {
unikery.setTaskId(Unikery.INVALID_ID);
}
task.clearUnikery();
onUnregisterConacoTask(task);
}
@IntDef({SOURCE_MEMORY, SOURCE_DISK, SOURCE_NETWORK})
@Retention(RetentionPolicy.SOURCE)
public @interface Source {
}
public static class Builder<T> extends BeerBelly.BeerBellyParams {
/**
* The client to get image from internet
*/
public OkHttpClient okHttpClient = null;
/**
* Decode, get size and others
*/
public ValueHelper<T> objectHelper = null;
public boolean debug = false;
@Override
public void isValid() throws IllegalStateException {
super.isValid();
if (okHttpClient == null) {
throw new IllegalStateException("No http client? How can I load image via url?");
}
}
public Conaco<T> build() {
isValid();
return new Conaco<>(this);
}
}
}
@@ -1,484 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.okhttp.ChromeRequestBuilder;
import com.hippo.streampipe.OutputStreamPipe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@SuppressWarnings({"rawtypes", "deprecation"})
public final class ConacoTask<V> {
private static final String TAG = ConacoTask.class.getSimpleName();
private final int mId;
private final WeakReference<Unikery<V>> mUnikeryWeakReference;
private final String mKey;
private final String mUrl;
private final boolean mUseMemoryCache;
private final boolean mUseDiskCache;
private final boolean mUseNetwork;
private final ValueHelper<V> mHelper;
private final ValueCache<V> mCache;
private final OkHttpClient mOkHttpClient;
private final Executor mDiskExecutor;
private final Executor mNetworkExecutor;
private final Conaco<V> mConaco;
private DiskLoadTask mDiskLoadTask;
private NetworkLoadTask mNetworkLoadTask;
private Call mCall;
private boolean mStart;
private volatile boolean mStop;
private ConacoTask(Builder<V> builder) {
mId = builder.mId;
mUnikeryWeakReference = new WeakReference<>(builder.mUnikery);
mKey = builder.mKey;
mUrl = builder.mUrl;
mUseMemoryCache = builder.mUseMemoryCache;
mUseDiskCache = builder.mUseDiskCache;
mUseNetwork = builder.mUseNetwork;
mHelper = builder.mHelper;
mCache = builder.mCache;
mOkHttpClient = builder.mOkHttpClient;
mDiskExecutor = builder.mDiskExecutor;
mNetworkExecutor = builder.mNetworkExecutor;
mConaco = builder.mConaco;
}
int getId() {
return mId;
}
String getKey() {
return mKey;
}
boolean useMemoryCache() {
return mUseMemoryCache;
}
@Nullable
Unikery<V> getUnikery() {
return mUnikeryWeakReference.get();
}
void clearUnikery() {
mUnikeryWeakReference.clear();
}
private void onFinish() {
if (!mStop) {
mConaco.finishConacoTask(this);
}/* else {
// It is done by Conaco
}*/
}
@UiThread
void start() {
if (mStop || mStart) {
return;
}
mStart = true;
Unikery unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
if (mUseDiskCache) {
mDiskLoadTask = new DiskLoadTask();
mDiskLoadTask.executeOnExecutor(mDiskExecutor);
return;
} else if (mUseNetwork) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onRequest();
mNetworkLoadTask = new NetworkLoadTask();
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
return;
} else {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onMiss(Conaco.SOURCE_NETWORK);
unikery.onFailure();
}
}
onFinish();
}
@UiThread
void stop() {
if (mStop) {
return;
}
mStop = true;
// Stop jobs
if (mDiskLoadTask != null) { // Getting from disk
mDiskLoadTask.cancel(false);
} else if (mNetworkLoadTask != null) { // Getting from network
mNetworkLoadTask.cancel(false);
if (mCall != null) {
mCall = null;
}
}
Unikery unikery = mUnikeryWeakReference.get();
if (unikery != null) {
unikery.onCancel();
}
// Conaco handle the clean up
}
private boolean isNotNecessary(AsyncTask asyncTask) {
Unikery unikery = mUnikeryWeakReference.get();
return mStop || asyncTask.isCancelled() || unikery == null || unikery.getTaskId() != mId;
}
@SuppressWarnings({"unused", "UnusedReturnValue", "RedundantSuppression"})
public static class Builder<T> {
private int mId;
private Unikery<T> mUnikery;
private String mKey;
private String mUrl;
private boolean mUseMemoryCache = true;
private boolean mUseDiskCache = true;
private boolean mUseNetwork = true;
private ValueHelper<T> mHelper;
private ValueCache<T> mCache;
private OkHttpClient mOkHttpClient;
private Executor mDiskExecutor;
private Executor mNetworkExecutor;
private Conaco<T> mConaco;
public Builder<T> setId(int id) {
mId = id;
return this;
}
public Unikery<T> getUnikery() {
return mUnikery;
}
public Builder<T> setUnikery(Unikery<T> unikery) {
mUnikery = unikery;
return this;
}
public String getKey() {
return mKey;
}
public Builder<T> setKey(String key) {
mKey = key;
return this;
}
public String getUrl() {
return mUrl;
}
public Builder<T> setUrl(String url) {
mUrl = url;
return this;
}
boolean isUseMemoryCache() {
return mUseMemoryCache;
}
public Builder<T> setUseMemoryCache(boolean useMemoryCache) {
mUseMemoryCache = useMemoryCache;
return this;
}
boolean isUseDiskCache() {
return mUseDiskCache;
}
public Builder<T> setUseDiskCache(boolean useDiskCache) {
mUseDiskCache = useDiskCache;
return this;
}
boolean isUseNetwork() {
return mUseNetwork;
}
public Builder<T> setUseNetwork(boolean useNetwork) {
mUseNetwork = useNetwork;
return this;
}
ValueHelper<T> getHelper() {
return mHelper;
}
Builder<T> setHelper(ValueHelper<T> helper) {
mHelper = helper;
return this;
}
Builder<T> setCache(ValueCache<T> cache) {
mCache = cache;
return this;
}
Builder<T> setOkHttpClient(OkHttpClient okHttpClient) {
mOkHttpClient = okHttpClient;
return this;
}
Builder<T> setDiskExecutor(Executor diskExecutor) {
mDiskExecutor = diskExecutor;
return this;
}
Builder<T> setNetworkExecutor(Executor networkExecutor) {
mNetworkExecutor = networkExecutor;
return this;
}
Builder<T> setConaco(Conaco<T> conaco) {
mConaco = conaco;
return this;
}
public void isValid() {
if (mUnikery == null) {
throw new IllegalStateException("Must set unikery");
}
if (mKey == null && mUrl == null) {
throw new IllegalStateException("At least one of mKey and mUrl and mDataContainer have to not be null");
}
}
public ConacoTask<T> build() {
return new ConacoTask<>(this);
}
}
@SuppressLint("StaticFieldLeak")
private class DiskLoadTask extends AsyncTask<Void, Void, V> {
@Override
protected V doInBackground(Void... params) {
if (isNotNecessary(this)) {
return null;
} else {
V value = null;
// Then check disk cache
if (mKey != null) {
if (value == null && mUseDiskCache) {
value = mCache.getFromDisk(mKey);
}
if (value != null && mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
mCache.putToMemory(mKey, value);
}
}
return value;
}
}
@Override
protected void onPostExecute(V value) {
mDiskLoadTask = null;
if (isCancelled() || mStop) {
onCancelled(value);
} else {
Unikery<V> unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
boolean getValue = false;
if ((value == null || !(getValue = unikery.onGetValue(value, Conaco.SOURCE_DISK))) && mUseNetwork) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onRequest();
mNetworkLoadTask = new NetworkLoadTask();
mNetworkLoadTask.executeOnExecutor(mNetworkExecutor);
return;
} else if (!getValue) {
unikery.onMiss(Conaco.SOURCE_DISK);
unikery.onFailure();
}
}
onFinish();
}
}
@Override
protected void onCancelled(V holder) {
onFinish();
}
}
@SuppressLint("StaticFieldLeak")
private class NetworkLoadTask extends AsyncTask<Void, Long, V> implements ProgressNotifier {
@Override
public void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize) {
if (!isNotNecessary(this)) {
publishProgress(singleReceivedSize, receivedSize, totalSize);
}
}
private boolean putToDiskCache(InputStream is, long length) {
SimpleDiskCache diskCache = mCache.getDiskCache();
if (diskCache == null) {
return false;
}
OutputStreamPipe pipe = diskCache.getOutputStreamPipe(mKey);
try {
pipe.obtain();
OutputStream os = pipe.open();
final byte[] buffer = new byte[1024 * 4];
long receivedSize = 0;
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
receivedSize += bytesRead;
notifyProgress(bytesRead, receivedSize, length);
}
return true;
} catch (IOException e) {
return false;
} finally {
pipe.close();
pipe.release();
}
}
@Override
protected V doInBackground(Void... params) {
if (isNotNecessary(this)) {
return null;
}
V value;
InputStream is = null;
try {
Log.d("TAG", "Conaco " + mUrl);
// Load it from internet
Request request = new ChromeRequestBuilder(mUrl).build();
mCall = mOkHttpClient.newCall(request);
Response response = mCall.execute();
ResponseBody body = response.body();
if (body == null) {
return null;
}
is = body.byteStream();
if (isNotNecessary(this)) {
return null;
}
if (mKey != null) {
if (putToDiskCache(is, body.contentLength())) {
// Get object from disk cache
value = mCache.getFromDisk(mKey);
if (value == null) {
// Maybe bad download, remove it from disk cache
mCache.removeFromDisk(mKey);
} else if (mUseMemoryCache && mHelper.useMemoryCache(mKey, value)) {
// Put it to memory
mCache.putToMemory(mKey, value);
}
return value;
} else {
// Maybe bad download, remove it from disk cache
mCache.removeFromDisk(mKey);
return null;
}
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
mCall = null;
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
// Ignore
}
}
}
@Override
protected void onPostExecute(V holder) {
mNetworkLoadTask = null;
if (isCancelled() || mStop) {
onCancelled(holder);
} else {
Unikery<V> unikery = mUnikeryWeakReference.get();
if (unikery != null && unikery.getTaskId() == mId) {
if (holder == null || !unikery.onGetValue(holder, Conaco.SOURCE_NETWORK)) {
unikery.onFailure();
}
}
onFinish();
}
}
@Override
protected void onCancelled(V value) {
onFinish();
}
@Override
protected void onProgressUpdate(Long... values) {
Unikery unikery = mUnikeryWeakReference.get();
if (!mStop && !isCancelled() && unikery != null && unikery.getTaskId() == mId) {
unikery.onProgress(values[0], values[1], values[2]);
}
}
}
}
@@ -1,22 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
public interface ProgressNotifier {
void notifyProgress(long singleReceivedSize, long receivedSize, long totalSize);
}
@@ -1,72 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import com.hippo.yorozuya.collect.Multimap;
import com.hippo.yorozuya.collect.SparseILArray;
import java.util.List;
class Register<V> {
private final SparseILArray<ConacoTask<V>> mIdMap = new SparseILArray<>();
private final Multimap<String, ConacoTask<V>> mKeyMap = Multimap.create();
/**
* @return true for the key is already registered
*/
public synchronized boolean register(int id, ConacoTask<V> task) {
boolean repeatedKey = false;
String taskKey = task.getKey();
// For no-key task, there no need to check
if (taskKey != null) {
repeatedKey = mKeyMap.countElements(taskKey) != 0;
}
// Append task
mKeyMap.putElement(taskKey, task);
mIdMap.append(id, task);
return repeatedKey;
}
public synchronized ConacoTask<V> unregister(int id) {
ConacoTask<V> task = mIdMap.remove(id);
if (task != null) {
mKeyMap.removeElement(task.getKey(), task);
}
return task;
}
public synchronized boolean contain(int id) {
return mIdMap.indexOfKey(id) >= 0;
}
public synchronized ConacoTask<V> getByKey(String key) {
if (key == null) {
return null;
}
List<ConacoTask<V>> list = mKeyMap.get(key);
if (list != null && list.size() > 0) {
return list.get(0);
} else {
return null;
}
}
}
@@ -1,77 +0,0 @@
/*
* Copyright 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import androidx.annotation.NonNull;
public interface Unikery<V> {
int INVALID_ID = -1;
/**
* Get the conaco task id
*
* @return the conaco task id
*/
int getTaskId();
/**
* Store the conaco task id
*
* @param id the conaco task id
*/
void setTaskId(int id);
/**
* On miss in the source
*/
void onMiss(@Conaco.Source int source);
/**
* On start http request
*/
void onRequest();
/**
* On http request progress
*/
void onProgress(long singleReceivedSize, long receivedSize, long totalSize);
/**
* On start to wait because repeated key
*/
void onWait();
/**
* On get the value
*
* @return {@code true} for the {@code Unikery} accepts the value
* and the task ends. {@code false} for the {@code Unikery}
* reject the value and the task goes on or failed
*/
boolean onGetValue(@NonNull V value, @Conaco.Source int source);
/**
* On failed to get value
*/
void onFailure();
/**
* On user cancel the conaco task
*/
void onCancel();
}
@@ -1,61 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import androidx.annotation.NonNull;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.streampipe.InputStreamPipe;
import java.io.OutputStream;
class ValueCache<V> extends BeerBelly<V> {
private final ValueHelper<V> mHelper;
public ValueCache(BeerBelly.BeerBellyParams params, ValueHelper<V> helper) {
super(params);
mHelper = helper;
}
@Override
protected int sizeOf(String key, V value) {
return mHelper.sizeOf(key, value);
}
@Override
protected void memoryEntryAdded(V value) {
mHelper.onAddToMemoryCache(value);
}
@Override
protected void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue) {
if (oldValue != null) {
mHelper.onRemoveFromMemoryCache(key, oldValue);
}
}
@Override
protected V read(@NonNull InputStreamPipe isPipe) {
return mHelper.decode(isPipe);
}
@Override
protected boolean write(OutputStream os, V value) {
throw new UnsupportedOperationException("Not support write object");
}
}
@@ -1,55 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.conaco;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.streampipe.InputStreamPipe;
public interface ValueHelper<V> {
/**
* Decode object for the {@code InputStreamPipe}
*
* @param isPipe the {@code InputStreamPipe}
* @return the decoded object
*/
@Nullable
V decode(@NonNull InputStreamPipe isPipe);
/**
* Get the size of the object
*/
int sizeOf(@NonNull String key, @NonNull V value);
/**
* Called when the object added to memory cache
*/
void onAddToMemoryCache(@NonNull V oldValue);
/**
* Called when the object removed from memory cache
*/
void onRemoveFromMemoryCache(@NonNull String key, @NonNull V oldValue);
/**
* Use cache memory or not. Sometimes large object
* should not be stored in memory cache.
*/
boolean useMemoryCache(@NonNull String key, V holder);
}
@@ -1,215 +0,0 @@
/*
* Copyright (C) 2015 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.drawable;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.Keep;
import com.hippo.ehviewer.R;
import com.hippo.yorozuya.MathUtils;
/**
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate between them.
*/
public class DrawerArrowDrawable extends Drawable {
// The angle in degrees that the arrow head is inclined at.
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
private final Paint mPaint = new Paint();
private final float mBarThickness;
// The length of top and bottom bars when they merge into an arrow
private final float mTopBottomArrowSize;
// The length of middle bar
private final float mBarSize;
// The length of the middle bar when arrow is shaped
private final float mMiddleArrowSize;
// The space between bars when they are parallel
private final float mBarGap;
// Whether bars should spin or not during progress
private final boolean mSpin;
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
// wont look different
private final Path mPath = new Path();
// The reported intrinsic size of the drawable.
private final int mSize;
// the amount that overlaps w/ bar size when rotation is max
private final float mMaxCutForBarSize;
// Whether we should mirror animation when animation is reversed.
private boolean mVerticalMirror = false;
// The interpolated version of the original progress
private float mProgress;
/**
* @param context used to get the configuration for the drawable from
*/
public DrawerArrowDrawable(Context context, int color) {
Resources resources = context.getResources();
mPaint.setAntiAlias(true);
mPaint.setColor(color);
mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size);
// round this because having this floating may cause bad measurements
mBarSize = Math.round(resources.getDimension(R.dimen.dad_bar_size));
// round this because having this floating may cause bad measurements
mTopBottomArrowSize = Math.round(resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size));
mBarThickness = resources.getDimension(R.dimen.dad_thickness);
// round this because having this floating may cause bad measurements
mBarGap = Math.round(resources.getDimension(R.dimen.dad_gap_between_bars));
mSpin = resources.getBoolean(R.bool.dad_spin_bars);
mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.MITER);
mPaint.setStrokeCap(Paint.Cap.BUTT);
mPaint.setStrokeWidth(mBarThickness);
mMaxCutForBarSize = (float) (mBarThickness / 2 * Math.cos(ARROW_HEAD_ANGLE));
}
/**
* If set, canvas is flipped when progress reached to end and going back to start.
*/
protected void setVerticalMirror(boolean verticalMirror) {
mVerticalMirror = verticalMirror;
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
// Interpolated widths of arrow bars
final float arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress);
final float middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress);
// Interpolated size of middle bar
final float middleBarCut = Math.round(MathUtils.lerp(0, mMaxCutForBarSize, mProgress));
// The rotation of the top and bottom bars (that make the arrow head)
final float rotation = MathUtils.lerp(0, ARROW_HEAD_ANGLE, mProgress);
// The whole canvas rotates as the transition happens
final float canvasRotate = MathUtils.lerp(-180, 0, mProgress);
final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
mPath.rewind();
final float topBottomBarOffset = MathUtils.lerp(mBarGap + mBarThickness, -mMaxCutForBarSize,
mProgress);
final float arrowEdge = -middleBarSize / 2;
// draw middle bar
mPath.moveTo(arrowEdge + middleBarCut, 0);
mPath.rLineTo(middleBarSize - middleBarCut * 2, 0);
// bottom bar
mPath.moveTo(arrowEdge, topBottomBarOffset);
mPath.rLineTo(arrowWidth, arrowHeight);
// top bar
mPath.moveTo(arrowEdge, -topBottomBarOffset);
mPath.rLineTo(arrowWidth, -arrowHeight);
mPath.close();
canvas.save();
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
// the arrow pointing the other way for RTL.
canvas.translate(bounds.centerX(), bounds.centerY());
if (mSpin) {
canvas.rotate(canvasRotate * (mVerticalMirror ? -1 : 1));
}
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
public void setColor(@ColorInt int color) {
mPaint.setColor(color);
invalidateSelf();
}
@Override
public void setAlpha(int i) {
mPaint.setAlpha(i);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Keep
public float getProgress() {
return mProgress;
}
@Keep
public void setProgress(float progress) {
if (progress == 1f) {
setVerticalMirror(true);
} else if (progress == 0f) {
setVerticalMirror(false);
}
mProgress = progress;
invalidateSelf();
}
public void setMenu(long duration) {
setShape(false, duration);
}
public void setArrow(long duration) {
setShape(true, duration);
}
public void setShape(boolean arrow, long duration) {
if (!((!arrow && mProgress == 0f) || (arrow && mProgress == 1f))) {
float endProgress = arrow ? 1f : 0f;
if (duration <= 0) {
setProgress(endProgress);
} else {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", endProgress);
oa.setDuration(duration);
oa.setAutoCancel(true);
oa.start();
}
}
}
}
@@ -18,31 +18,23 @@ package com.hippo.drawable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.annotation.NonNull;
import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.Unikery;
import com.hippo.image.ImageBitmap;
import com.hippo.widget.ObservedTextView;
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
ObservedTextView.OnWindowAttachListener {
import coil.Coil;
import coil.request.Disposable;
import coil.request.ImageRequest;
public class UnikeryDrawable extends WrapDrawable implements ObservedTextView.OnWindowAttachListener {
private static final String TAG = UnikeryDrawable.class.getSimpleName();
private final ObservedTextView mTextView;
private final Conaco<ImageBitmap> mConaco;
private int mTaskId = Unikery.INVALID_ID;
private String mUrl;
private Disposable task;
private ImageBitmap imageBitmap;
public UnikeryDrawable(ObservedTextView textView, Conaco<ImageBitmap> conaco) {
public UnikeryDrawable(ObservedTextView textView) {
mTextView = textView;
mTextView.setOnWindowAttachListener(this);
mConaco = conaco;
}
@Override
@@ -52,23 +44,29 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
@Override
public void onDetachedFromWindow() {
mConaco.cancel(this);
if (task != null && !task.isDisposed())
task.dispose();
clearDrawable();
}
public void load(String url) {
if (url != null) {
mUrl = url;
mConaco.load(new ConacoTask.Builder<ImageBitmap>().setUnikery(this).setUrl(url).setKey(url));
var imageLoader = Coil.imageLoader(mTextView.getContext());
var request = new ImageRequest.Builder(mTextView.getContext()).data(url).memoryCacheKey(url).diskCacheKey(url).target(
drawable -> null,
drawable -> null,
drawable -> {
onGetValue(drawable);
return null;
}
).build();
task = imageLoader.enqueue(request);
}
}
private void clearDrawable() {
setDrawable(null);
if (imageBitmap != null) {
imageBitmap.release();
imageBitmap = null;
}
}
@Override
@@ -91,65 +89,16 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
}
}
@Override
public int getTaskId() {
return mTaskId;
}
@Override
public void setTaskId(int id) {
mTaskId = id;
}
@Override
public void invalidateSelf() {
CharSequence cs = mTextView.getText();
mTextView.setText(cs);
}
@Override
public void onMiss(int source) {
}
@Override
public void onRequest() {
}
@Override
public void onProgress(long singleReceivedSize, long receivedSize, long totalSize) {
}
@Override
public void onWait() {
}
@Override
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
Drawable drawable;
try {
drawable = value.getDrawable();
} catch (Exception e) {
Log.d(TAG, "The ImageBitmap is recycled", e);
return false;
}
public void onGetValue(Drawable drawable) {
clearDrawable();
setDrawable(drawable);
imageBitmap = value;
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable)
animatedImageDrawable.start();
return true;
}
@Override
public void onFailure() {
// Empty
}
@Override
public void onCancel() {
// Empty
}
}
@@ -1,593 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Debug;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.collection.LruCache;
import androidx.core.os.LocaleListCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.hippo.Native;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.beerbelly.SimpleDiskCache;
import com.hippo.conaco.Conaco;
import com.hippo.ehviewer.client.EhClient;
import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhDns;
import com.hippo.ehviewer.client.EhEngine;
import com.hippo.ehviewer.client.EhRequestBuilder;
import com.hippo.ehviewer.client.EhSSLSocketFactory;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.EhX509TrustManager;
import com.hippo.ehviewer.client.data.GalleryDetail;
import com.hippo.ehviewer.client.parser.EventPaneParser;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.EhActivity;
import com.hippo.image.ImageBitmap;
import com.hippo.network.StatusCodeException;
import com.hippo.scene.SceneApplication;
import com.hippo.unifile.UniFile;
import com.hippo.util.BitmapUtils;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.IoThreadPoolExecutor;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.OSUtils;
import com.hippo.yorozuya.SimpleHandler;
import java.io.File;
import java.io.IOException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class EhApplication extends SceneApplication {
public static final boolean BETA = false;
private static final String TAG = EhApplication.class.getSimpleName();
private static final String KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id";
private static final boolean DEBUG_CONACO = false;
private static final boolean DEBUG_PRINT_NATIVE_MEMORY = false;
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
private static final long DEBUG_PRINT_INTERVAL = 3000L;
public static boolean locked = true;
public static long locked_last_leave_time = 0;
private static EhApplication instance;
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
private final HashMap<Integer, Object> mGlobalStuffMap = new HashMap<>();
private final List<Activity> mActivityList = new ArrayList<>();
private EhCookieStore mEhCookieStore;
private EhClient mEhClient;
private EhProxySelector mEhProxySelector;
private OkHttpClient mOkHttpClient;
private Cache mOkHttpCache;
private ImageBitmapHelper mImageBitmapHelper;
private Conaco<ImageBitmap> mConaco;
private LruCache<Long, GalleryDetail> mGalleryDetailCache;
private SimpleDiskCache mSpiderInfoCache;
private DownloadManager mDownloadManager;
private Hosts mHosts;
private FavouriteStatusRouter mFavouriteStatusRouter;
private boolean initialized = false;
public static EhApplication getInstance() {
return instance;
}
public static EhCookieStore getEhCookieStore(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhCookieStore == null) {
application.mEhCookieStore = new EhCookieStore(context);
}
return application.mEhCookieStore;
}
@NonNull
public static EhClient getEhClient(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhClient == null) {
application.mEhClient = new EhClient(application);
}
return application.mEhClient;
}
@NonNull
public static EhProxySelector getEhProxySelector(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mEhProxySelector == null) {
application.mEhProxySelector = new EhProxySelector();
}
return application.mEhProxySelector;
}
@NonNull
public static OkHttpClient getOkHttpClient(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.cookieJar(getEhCookieStore(application))
.cache(getOkHttpCache(application))
.dns(new EhDns(application))
.addNetworkInterceptor(chain -> {
try {
return chain.proceed(chain.request());
} catch (NullPointerException e) {
// crash on meizu devices due to old Android version
// https://github.com/square/okhttp/issues/3301#issuecomment-348415095
throw new IOException(e.getMessage());
}
})
.proxySelector(getEhProxySelector(application));
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager trustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
builder.sslSocketFactory(new EhSSLSocketFactory(), trustManager);
} catch (Exception e) {
e.printStackTrace();
builder.sslSocketFactory(new EhSSLSocketFactory(), new EhX509TrustManager());
}
application.mOkHttpClient = builder.build();
}
return application.mOkHttpClient;
}
@NonNull
public static Cache getOkHttpCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mOkHttpCache == null) {
application.mOkHttpCache = new Cache(new File(application.getCacheDir(), "http_cache"), 50L * 1024L * 1024L);
}
return application.mOkHttpCache;
}
@NonNull
public static ImageBitmapHelper getImageBitmapHelper(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mImageBitmapHelper == null) {
application.mImageBitmapHelper = new ImageBitmapHelper();
}
return application.mImageBitmapHelper;
}
private static int getMemoryCacheMaxSize() {
return Math.min(20 * 1024 * 1024, (int) OSUtils.getAppMaxMemory());
}
@NonNull
public static Conaco<ImageBitmap> getConaco(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mConaco == null) {
Conaco.Builder<ImageBitmap> builder = new Conaco.Builder<>();
builder.hasMemoryCache = true;
builder.memoryCacheMaxSize = getMemoryCacheMaxSize();
builder.hasDiskCache = true;
builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
builder.diskCacheMaxSize = 320 * 1024 * 1024; // 320MB
builder.okHttpClient = getOkHttpClient(context);
builder.objectHelper = getImageBitmapHelper(context);
builder.debug = DEBUG_CONACO;
application.mConaco = builder.build();
}
return application.mConaco;
}
@NonNull
public static LruCache<Long, GalleryDetail> getGalleryDetailCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mGalleryDetailCache == null) {
// Max size 25, 3 min timeout
application.mGalleryDetailCache = new LruCache<>(25);
getFavouriteStatusRouter().addListener((gid, slot) -> {
GalleryDetail gd = application.mGalleryDetailCache.get(gid);
if (gd != null) {
gd.favoriteSlot = slot;
}
});
}
return application.mGalleryDetailCache;
}
@NonNull
public static SimpleDiskCache getSpiderInfoCache(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (null == application.mSpiderInfoCache) {
application.mSpiderInfoCache = new SimpleDiskCache(
new File(context.getCacheDir(), "spider_info"), 20 * 1024 * 1024); // 20M
}
return application.mSpiderInfoCache;
}
@NonNull
public static DownloadManager getDownloadManager() {
return getDownloadManager(instance);
}
@NonNull
public static DownloadManager getDownloadManager(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mDownloadManager == null) {
application.mDownloadManager = new DownloadManager(application);
}
return application.mDownloadManager;
}
@NonNull
public static Hosts getHosts(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mHosts == null) {
application.mHosts = new Hosts(application, "hosts.db");
}
return application.mHosts;
}
@NonNull
public static FavouriteStatusRouter getFavouriteStatusRouter() {
return getFavouriteStatusRouter(getInstance());
}
@NonNull
public static FavouriteStatusRouter getFavouriteStatusRouter(@NonNull Context context) {
EhApplication application = ((EhApplication) context.getApplicationContext());
if (application.mFavouriteStatusRouter == null) {
application.mFavouriteStatusRouter = new FavouriteStatusRouter();
}
return application.mFavouriteStatusRouter;
}
@SuppressLint("StaticFieldLeak")
@Override
public void onCreate() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
instance = this;
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
try {
// Always save crash file if onCreate() is not done
if (!initialized || Settings.getSaveCrashLog()) {
Crash.saveCrashLog(instance, e);
}
} catch (Throwable ignored) {
}
if (handler != null) {
handler.uncaughtException(t, e);
}
});
super.onCreate();
Native.initialize();
GetText.initialize(this);
StatusCodeException.initialize(this);
Settings.initialize(this);
ReadableTime.initialize(this);
AppConfig.initialize(this);
SpiderDen.initialize(this);
EhDB.initialize(this);
EhEngine.initialize();
BitmapUtils.initialize(this);
// Locales can be managed by system automatically above Snow Cone v2
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
// Do io tasks in new thread
//noinspection deprecation
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
// Check no media file
try {
UniFile downloadLocation = Settings.getDownloadLocation();
if (Settings.getMediaScan()) {
CommonOperations.removeNoMediaFile(downloadLocation);
} else {
CommonOperations.ensureNoMediaFile(downloadLocation);
}
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
// Clear temp files
try {
clearTempDir();
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
return null;
}
}.executeOnExecutor(IoThreadPoolExecutor.getInstance());
// Check app update
update();
// Update version code
try {
PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0);
Settings.putVersionCode(pi.versionCode);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0));
if (DEBUG_PRINT_NATIVE_MEMORY || DEBUG_PRINT_IMAGE_COUNT) {
debugPrint();
}
initialized = true;
theDawnOfNewDay();
}
private void theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return;
}
EhCookieStore store = getEhCookieStore(this);
HttpUrl eh = HttpUrl.get(EhUrl.HOST_E);
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(eh, EhCookieStore.KEY_IPD_PASS_HASH)) {
IoThreadPoolExecutor.getInstance().execute(() -> {
String referer = EhUrl.REFERER_E;
Request request = new EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build();
Call call = getOkHttpClient(this).newCall(request);
try {
Response response = call.execute();
ResponseBody responseBody = response.body();
if (responseBody == null) {
return;
}
String body = responseBody.string();
String html = EventPaneParser.parse(body);
if (html != null) {
showEventPane(html);
}
} catch (Throwable e) {
e.printStackTrace();
}
});
}
}
public void showEventPane(String html) {
Activity activity = getTopActivity();
if (activity != null) {
activity.runOnUiThread(() -> {
AlertDialog dialog = new BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html))
.setPositiveButton(android.R.string.ok, null)
.create();
dialog.setOnShowListener(d -> {
final View messageView = dialog.findViewById(android.R.id.message);
if (messageView instanceof TextView) {
((TextView) messageView).setMovementMethod(LinkMovementMethod.getInstance());
}
});
try {
dialog.show();
} catch (Throwable t) {
// ignore
}
});
}
}
private void clearTempDir() {
File dir = AppConfig.getTempDir();
if (null != dir) {
FileUtils.deleteContent(dir);
}
dir = AppConfig.getExternalTempDir();
if (null != dir) {
FileUtils.deleteContent(dir);
}
}
private void update() {
int version = Settings.getVersionCode();
if (version < 52) {
Settings.putGuideGallery(true);
}
}
public void clearMemoryCache() {
if (null != mConaco) {
mConaco.getBeerBelly().clearMemory();
}
if (null != mGalleryDetailCache) {
mGalleryDetailCache.evictAll();
}
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
clearMemoryCache();
}
}
private void debugPrint() {
new Runnable() {
@Override
public void run() {
if (DEBUG_PRINT_NATIVE_MEMORY) {
Log.i(TAG, "Native memory: " + FileUtils.humanReadableByteCount(
Debug.getNativeHeapAllocatedSize(), false));
}
SimpleHandler.getInstance().postDelayed(this, DEBUG_PRINT_INTERVAL);
}
}.run();
}
public int putGlobalStuff(@NonNull Object o) {
int id = mIdGenerator.nextId();
mGlobalStuffMap.put(id, o);
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId());
return id;
}
public boolean containGlobalStuff(int id) {
return mGlobalStuffMap.containsKey(id);
}
public Object getGlobalStuff(int id) {
return mGlobalStuffMap.get(id);
}
public Object removeGlobalStuff(int id) {
return mGlobalStuffMap.remove(id);
}
public void removeGlobalStuff(Object o) {
mGlobalStuffMap.values().removeAll(Collections.singleton(o));
}
public void registerActivity(Activity activity) {
mActivityList.add(activity);
}
public void unregisterActivity(Activity activity) {
mActivityList.remove(activity);
}
@Nullable
public EhActivity getTopActivity() {
if (!mActivityList.isEmpty()) {
return (EhActivity) mActivityList.get(mActivityList.size() - 1);
} else {
return null;
}
}
// Avoid crash on some "energy saving" devices
@Override
public ComponentName startService(Intent service) {
try {
return super.startService(service);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return null;
}
}
// Avoid crash on some "energy saving" devices
@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
try {
return super.bindService(service, conn, flags);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return false;
}
}
// Avoid crash on some "energy saving" devices
@Override
public void unbindService(ServiceConnection conn) {
try {
super.unbindService(conn);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
}
}
// Avoid crash on some "energy saving" devices
@Override
public ComponentName startForegroundService(Intent service) {
try {
return super.startForegroundService(service);
} catch (Throwable t) {
t.printStackTrace();
ExceptionUtils.throwIfFatal(t);
return null;
}
}
static class EhlifecycleObserver implements DefaultLifecycleObserver {
@Override
public void onPause(@NonNull LifecycleOwner owner) {
if (!locked)
locked_last_leave_time = System.currentTimeMillis() / 1000;
locked = true;
}
}
}
@@ -0,0 +1,326 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentCallbacks2
import android.os.Build
import android.text.Html
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.LruCache
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import coil.Coil
import coil.ImageLoader
import com.hippo.Native
import com.hippo.app.BaseDialogBuilder
import com.hippo.beerbelly.SimpleDiskCache
import com.hippo.ehviewer.client.EhClient
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhDns
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhRequestBuilder
import com.hippo.ehviewer.client.EhSSLSocketFactory
import com.hippo.ehviewer.client.EhUrl
import com.hippo.ehviewer.client.EhX509TrustManager
import com.hippo.ehviewer.client.data.GalleryDetail
import com.hippo.ehviewer.client.parser.EventPaneParser
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.spider.SpiderDen
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.EhActivity
import com.hippo.scene.SceneApplication
import com.hippo.util.BitmapUtils
import com.hippo.util.ExceptionUtils
import com.hippo.util.IoThreadPoolExecutor
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.IntIdGenerator
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.io.File
import java.security.KeyStore
import java.util.Arrays
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class EhApplication : SceneApplication() {
private val mIdGenerator = IntIdGenerator()
private val mGlobalStuffMap = HashMap<Int, Any>()
private val mActivityList = ArrayList<Activity>()
private var initialized = false
val topActivity: EhActivity?
get() = if (mActivityList.isNotEmpty()) {
mActivityList[mActivityList.size - 1] as EhActivity
} else {
null
}
fun recreateAllActivity() {
mActivityList.forEach { it.recreate() }
}
@SuppressLint("StaticFieldLeak")
override fun onCreate() {
ProcessLifecycleOwner.get().lifecycle.addObserver(EhlifecycleObserver())
application = this
val handler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
try {
if (Settings.getSaveCrashLog()) {
Crash.saveCrashLog(application, e)
}
} catch (ignored: Throwable) {
}
handler?.uncaughtException(t, e)
}
super.onCreate()
Native.initialize()
GetText.initialize(this)
Settings.initialize(this)
ReadableTime.initialize(this)
AppConfig.initialize(this)
SpiderDen.initialize(this)
EhDB.initialize(this)
EhEngine.initialize()
BitmapUtils.initialize(this)
Coil.setImageLoader(ImageLoader.Builder(this).okHttpClient(okHttpClient).build())
// Locales can be managed by system automatically above Snow Cone v2
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()))
AppCompatDelegate.setDefaultNightMode(Settings.getTheme())
IoThreadPoolExecutor.getInstance().execute {
try {
val downloadLocation = Settings.getDownloadLocation()
if (Settings.getMediaScan()) {
CommonOperations.removeNoMediaFile(downloadLocation)
} else {
CommonOperations.ensureNoMediaFile(downloadLocation)
}
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
}
// Clear temp files
try {
clearTempDir()
} catch (t: Throwable) {
t.printStackTrace()
ExceptionUtils.throwIfFatal(t)
}
}
mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))
initialized = true
theDawnOfNewDay()
}
private fun theDawnOfNewDay() {
if (!Settings.getRequestNews()) {
return
}
val store = ehCookieStore
val eh = EhUrl.HOST_E.toHttpUrl()
if (store.contains(eh, EhCookieStore.KEY_IPD_MEMBER_ID) || store.contains(
eh,
EhCookieStore.KEY_IPD_PASS_HASH
)
) {
IoThreadPoolExecutor.getInstance().execute {
val referer = EhUrl.REFERER_E
val request = EhRequestBuilder(EhUrl.HOST_E + "news.php", referer).build()
val call = okHttpClient.newCall(request)
try {
val response = call.execute()
val responseBody = response.body
val body = responseBody.string()
val html = EventPaneParser.parse(body)
if (html != null) {
showEventPane(html)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
}
fun showEventPane(html: String) {
val activity = topActivity
activity?.runOnUiThread {
val dialog = BaseDialogBuilder(activity)
.setMessage(Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton(android.R.string.ok, null)
.create()
dialog.setOnShowListener {
val messageView = dialog.findViewById<View>(android.R.id.message)
if (messageView is TextView) {
messageView.movementMethod = LinkMovementMethod.getInstance()
}
}
try {
dialog.show()
} catch (t: Throwable) {
// ignore
}
}
}
private fun clearTempDir() {
var dir = AppConfig.getTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
dir = AppConfig.getExternalTempDir()
if (null != dir) {
FileUtils.deleteContent(dir)
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
galleryDetailCache.evictAll()
}
}
fun putGlobalStuff(o: Any): Int {
val id = mIdGenerator.nextId()
mGlobalStuffMap[id] = o
Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())
return id
}
fun containGlobalStuff(id: Int): Boolean {
return mGlobalStuffMap.containsKey(id)
}
fun getGlobalStuff(id: Int): Any? {
return mGlobalStuffMap[id]
}
fun removeGlobalStuff(id: Int): Any? {
return mGlobalStuffMap.remove(id)
}
fun removeGlobalStuff(o: Any) {
mGlobalStuffMap.values.removeAll(setOf(o))
}
fun registerActivity(activity: Activity) {
mActivityList.add(activity)
}
fun unregisterActivity(activity: Activity) {
mActivityList.remove(activity)
}
internal class EhlifecycleObserver : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
if (!locked)
locked_last_leave_time = System.currentTimeMillis() / 1000
locked = true
}
}
companion object {
private const val KEY_GLOBAL_STUFF_NEXT_ID = "global_stuff_next_id"
var locked = true
var locked_last_leave_time: Long = 0
@JvmStatic
lateinit var application: EhApplication
private set
@JvmStatic
val ehCookieStore by lazy { EhCookieStore(application) }
@JvmStatic
val ehClient by lazy { EhClient(application) }
@JvmStatic
val ehProxySelector by lazy { EhProxySelector() }
@JvmStatic
val okHttpClient by lazy {
val builder = OkHttpClient.Builder()
.cookieJar(ehCookieStore)
.cache(Cache(File(application.cacheDir, "http_cache"), 50L * 1024L * 1024L))
.dns(EhDns())
.proxySelector(ehProxySelector)
if (Settings.getDF()) {
var trustManager: X509TrustManager
try {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {
"Unexpected default trust managers:" + Arrays.toString(
trustManagers
)
}
trustManager = trustManagers[0] as X509TrustManager
} catch (e: Exception) {
e.printStackTrace()
trustManager = EhX509TrustManager()
}
builder.sslSocketFactory(EhSSLSocketFactory(), trustManager)
}
builder.build()
}
@JvmStatic
val galleryDetailCache by lazy {
LruCache<Long, GalleryDetail>(25).also {
favouriteStatusRouter.addListener { gid, slot ->
it[gid]?.favoriteSlot = slot
}
}
}
@JvmStatic
val spiderInfoCache by lazy {
SimpleDiskCache(
File(application.cacheDir, "spider_info"),
20 * 1024 * 1024
)
}
@JvmStatic
val downloadManager by lazy { DownloadManager(application) }
@JvmStatic
val hosts by lazy { Hosts(application, "hosts.db") }
@JvmStatic
val favouriteStatusRouter by lazy { FavouriteStatusRouter() }
}
}
@@ -491,7 +491,7 @@ public class EhDB {
.createFromFile(file).allowMainThreadQueries().build();
// Download label
DownloadManager manager = EhApplication.getDownloadManager(context);
DownloadManager manager = EhApplication.getDownloadManager();
try {
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
manager.addDownloadLabel(downloadLabelList);
@@ -1,72 +0,0 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.conaco.ValueHelper;
import com.hippo.image.ImageBitmap;
import com.hippo.streampipe.InputStreamPipe;
import java.io.FileInputStream;
import java.io.IOException;
public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
private static final int MAX_CACHE_SIZE = 512 * 512;
@Nullable
@Override
public ImageBitmap decode(@NonNull InputStreamPipe isPipe) {
try {
isPipe.obtain();
FileInputStream is = (FileInputStream) isPipe.open();
return ImageBitmap.decode(is);
} catch (OutOfMemoryError | IOException e) {
return null;
} finally {
isPipe.close();
isPipe.release();
}
}
@Override
public int sizeOf(@NonNull String key, @NonNull ImageBitmap value) {
return value.getWidth() * value.getHeight() * 4 /* value.getByteCount() TODO Update Image */;
}
@Override
public void onAddToMemoryCache(@NonNull ImageBitmap oldValue) {
oldValue.obtain();
}
@Override
public void onRemoveFromMemoryCache(@NonNull String key, @NonNull ImageBitmap oldValue) {
oldValue.release();
}
@Override
public boolean useMemoryCache(@NonNull String key, ImageBitmap value) {
if (value != null) {
return value.getWidth() * value.getHeight() <= MAX_CACHE_SIZE
/* value.getByteCount() <= MAX_CACHE_BYTE_COUNT TODO Update Image */;
} else {
return true;
}
}
}
@@ -60,11 +60,11 @@ public class Settings {
public static final String KEY_GALLERY_SITE = "gallery_site";
public static final String KEY_LIST_MODE = "list_mode";
public static final String KEY_DETAIL_SIZE = "detail_size";
public static final String KEY_THUMB_SIZE = "thumb_size";
public static final String KEY_THUMB_SIZE = "thumb_size_";
public static final String KEY_THUMB_RESOLUTION = "thumb_resolution";
public static final String KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations";
public static final String KEY_DEFAULT_CATEGORIES = "default_categories";
public static final int DEFAULT_DEFAULT_CATEGORIES = EhUtils.ALL_CATEGORY;
public static final int DEFAULT_DEFAULT_CATEGORIES = EhConfig.ALL_CATEGORY;
public static final String KEY_EXCLUDED_TAG_NAMESPACES = "excluded_tag_namespaces";
public static final String KEY_EXCLUDED_LANGUAGES = "excluded_languages";
/********************
@@ -78,7 +78,6 @@ public class Settings {
public static final String KEY_DOWNLOAD_SAVE_SCHEME = "image_scheme";
public static final String KEY_DOWNLOAD_SAVE_AUTHORITY = "image_authority";
public static final String KEY_DOWNLOAD_SAVE_PATH = "image_path";
public static final String KEY_NOTIFICATION_REQUIRED = "notification_required";
public static final String KEY_DOWNLOAD_SAVE_QUERY = "image_query";
public static final String KEY_DOWNLOAD_SAVE_FRAGMENT = "image_fragment";
@@ -101,6 +100,10 @@ public class Settings {
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_APP_LANGUAGE = "app_language";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String KEY_SHOW_COMMENTS = "show_gallery_comments";
private static final boolean DEFAULT_SHOW_COMMENTS = true;
private static final String KEY_PREVIEW_NUM = "preview_num";
private static final int DEFAULT_PREVIEW_NUM = 60;
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_VERSION_CODE = "version_code";
private static final int DEFAULT_VERSION_CODE = 0;
@@ -126,7 +129,7 @@ public class Settings {
private static final int DEFAULT_LAUNCH_PAGE = 0;
private static final int DEFAULT_LIST_MODE = 0;
private static final int DEFAULT_DETAIL_SIZE = 0;
private static final int DEFAULT_THUMB_SIZE = 1;
private static final int DEFAULT_THUMB_SIZE = 120;
private static final int DEFAULT_THUMB_RESOLUTION = 0;
private static final String KEY_SHOW_JPN_TITLE = "show_jpn_title";
private static final boolean DEFAULT_SHOW_JPN_TITLE = false;
@@ -243,7 +246,6 @@ public class Settings {
****** Update
********************/
private static final String KEY_BETA_UPDATE_CHANNEL = "beta_update_channel";
private static final boolean DEFAULT_BETA_UPDATE_CHANNEL = EhApplication.BETA;
private static final String KEY_SKIP_UPDATE_VERSION = "skip_update_version";
private static final int DEFAULT_SKIP_UPDATE_VERSION = 0;
private static final boolean DEFAULT_SAVE_PARSE_ERROR_BODY = true;
@@ -421,14 +423,6 @@ public class Settings {
sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
}
public static int getVersionCode() {
return getInt(KEY_VERSION_CODE, DEFAULT_VERSION_CODE);
}
public static void putVersionCode(int value) {
putInt(KEY_VERSION_CODE, value);
}
@Nullable
public static String getDisplayName() {
return getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME);
@@ -537,26 +531,22 @@ public class Settings {
}
public static int getThumbSize() {
return getIntFromStr(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE);
return dip2px(getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE));
}
@DimenRes
public static int getThumbSizeResId() {
switch (getThumbSize()) {
case 0:
return R.dimen.gallery_grid_column_width_large;
default:
case 1:
return R.dimen.gallery_grid_column_width_middle;
case 2:
return R.dimen.gallery_grid_column_width_small;
}
public static int dip2px(int dpValue) {
final float scale = sContext.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
public static int getThumbResolution() {
return getIntFromStr(KEY_THUMB_RESOLUTION, DEFAULT_THUMB_RESOLUTION);
}
public static boolean getShowComments() {
return getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS);
}
public static boolean getRequestNews() {
return getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS);
}
@@ -577,6 +567,10 @@ public class Settings {
putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value);
}
public static int getPreviewNum() {
return getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM);
}
public static int getDefaultCategories() {
return getInt(KEY_DEFAULT_CATEGORIES, DEFAULT_DEFAULT_CATEGORIES);
}
@@ -1021,10 +1015,6 @@ public class Settings {
return true;
}
public static boolean getBetaUpdateChannel() {
return getBoolean(KEY_BETA_UPDATE_CHANNEL, DEFAULT_BETA_UPDATE_CHANNEL);
}
public static void putBetaUpdateChannel(boolean value) {
putBoolean(KEY_BETA_UPDATE_CHANNEL, value);
}
@@ -61,7 +61,7 @@ public class EhClient {
public EhClient(Context context) {
mRequestThreadPool = IoThreadPoolExecutor.getInstance();
mOkHttpClient = EhApplication.getOkHttpClient(context);
mOkHttpClient = EhApplication.getOkHttpClient();
}
public void execute(EhRequest request) {
@@ -15,7 +15,6 @@
*/
package com.hippo.ehviewer.client
import android.content.Context
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.Hosts
import com.hippo.ehviewer.Settings
@@ -23,8 +22,8 @@ import okhttp3.Dns
import java.net.InetAddress
import java.net.UnknownHostException
class EhDns(context: Context) : Dns {
private val hosts = EhApplication.getHosts(context)
class EhDns : Dns {
private val hosts = EhApplication.hosts
private val builtInHosts: MutableMap<String, List<InetAddress>> = mutableMapOf()
init {
@@ -44,7 +43,7 @@ class EhDns(context: Context) : Dns {
builtInHosts,
"e-hentai.org",
Pair("104.20.134.21", true),
Pair("104.20.135.21", true),
Pair("104.20.135.21", false),
Pair("172.67.0.127", false),
)
put(
@@ -361,7 +361,7 @@ public class EhEngine {
body = response.body().string();
String html = EventPaneParser.parse(body);
if (html != null) {
EhApplication.getInstance().showEventPane(html);
EhApplication.getApplication().showEventPane(html);
}
return GalleryDetailParser.parse(body);
} catch (Throwable e) {
@@ -1,79 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class EhSSLSocketFactory extends SSLSocketFactory {
@Override
public String[] getDefaultCipherSuites() {
return ((SSLSocketFactory) getDefault()).getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return ((SSLSocketFactory) getDefault()).getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault();
if (!Settings.getDF()) {
return defaultFactory.createSocket(s, host, port, autoClose);
}
String address = s.getInetAddress().getHostAddress();
SSLSocket socket = (SSLSocket) defaultFactory.createSocket(s, address, port, autoClose);
SSLSession sslSession = socket.getSession();
Log.d("EhSSLSocketFactory", "Host: " + host + " Address: " + address + " Protocol:" + sslSession.getProtocol() + " CipherSuite:" + sslSession.getCipherSuite());
socket.setEnabledProtocols(socket.getSupportedProtocols());
return socket;
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return getDefault().createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return getDefault().createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return getDefault().createSocket(address, port, localAddress, localPort);
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client
import android.util.Log
import java.net.InetAddress
import java.net.Socket
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
class EhSSLSocketFactory : SSLSocketFactory() {
private val sslSocketFactory = getDefault() as SSLSocketFactory
override fun getDefaultCipherSuites(): Array<String> {
return sslSocketFactory.defaultCipherSuites
}
override fun getSupportedCipherSuites(): Array<String> {
return sslSocketFactory.supportedCipherSuites
}
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val address = s.inetAddress.hostAddress
val socket = sslSocketFactory.createSocket(s, address, port, autoClose) as SSLSocket
val sslSession = socket.session
Log.d(
"EhSSLSocketFactory",
"Host: " + host + " Address: " + address + " Protocol:" + sslSession.protocol + " CipherSuite:" + sslSession.cipherSuite
)
return socket
}
override fun createSocket(host: String, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
host: String,
port: Int,
localHost: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(host, port, localHost, localPort)
}
override fun createSocket(host: InetAddress, port: Int): Socket {
return sslSocketFactory.createSocket(host, port)
}
override fun createSocket(
address: InetAddress,
port: Int,
localAddress: InetAddress,
localPort: Int
): Socket {
return sslSocketFactory.createSocket(address, port, localAddress, localPort)
}
}
@@ -260,7 +260,7 @@ public class EhTagDatabase {
}
}
OkHttpClient client = EhApplication.getOkHttpClient(context);
OkHttpClient client = EhApplication.getOkHttpClient();
// Save new sha1
File tempSha1File = new File(dir, sha1Name + ".tmp");
@@ -32,10 +32,8 @@ import java.util.regex.Pattern;
public class EhUtils {
public static final int NONE = -1; // Use it for homepage
public static final int UNKNOWN = 0x400;
public static final int ALL_CATEGORY = EhUtils.UNKNOWN - 1;
//DOUJINSHI|MANGA|ARTIST_CG|GAME_CG|WESTERN|NON_H|IMAGE_SET|COSPLAY|ASIAN_PORN|MISC;
public static final int PRIVATE = 0x400;
public static final int UNKNOWN = 0x800;
public static final int BG_COLOR_DOUJINSHI = 0xfff44336;
public static final int BG_COLOR_MANGA = 0xffff9800;
@@ -68,6 +66,7 @@ public class EhUtils {
EhConfig.ASIAN_PORN,
EhConfig.NON_H,
EhConfig.WESTERN,
PRIVATE,
UNKNOWN};
private static final String[][] CATEGORY_STRINGS = {
@@ -81,6 +80,7 @@ public class EhUtils {
new String[]{"asianporn", "Asian Porn"},
new String[]{"non-h"},
new String[]{"western"},
new String[]{"private"},
new String[]{"unknown"}
};
@@ -133,14 +133,14 @@ public class EhUtils {
}
public static void signOut(Context context) {
EhApplication.getEhCookieStore(context).signOut();
EhApplication.getEhCookieStore().signOut();
Settings.putAvatar(null);
Settings.putDisplayName(null);
Settings.putNeedSignIn(true);
}
public static boolean needSignedIn(Context context) {
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore(context).hasSignedIn();
return Settings.getNeedSignIn() && !EhApplication.getEhCookieStore().hasSignedIn();
}
public static String getSuitableTitle(GalleryInfo gi) {
@@ -15,30 +15,23 @@
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.client
package com.hippo.ehviewer.client;
import android.annotation.SuppressLint
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
import android.annotation.SuppressLint;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
public class EhX509TrustManager implements X509TrustManager {
@SuppressLint("CustomX509TrustManager")
class EhX509TrustManager : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
override fun getAcceptedIssuers(): Array<X509Certificate?> {
return arrayOfNulls(0)
}
}
@@ -399,6 +399,9 @@ open class ListUrlBuilder : Cloneable, Parcelable {
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
@@ -416,6 +419,9 @@ open class ListUrlBuilder : Cloneable, Parcelable {
if (mNextGid != 0) {
sb.append("?next=").append(mNextGid)
}
if (mJumpTo != null) {
sb.append("&seek=").append(mJumpTo)
}
sb.toString()
}
@@ -46,14 +46,12 @@ import org.jsoup.select.Elements;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -64,9 +62,6 @@ public class GalleryDetailParser {
private static final Pattern PATTERN_TORRENT = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a");
private static final Pattern PATTERN_ARCHIVE = Pattern.compile("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>");
private static final Pattern PATTERN_COVER = Pattern.compile("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)");
private static final Pattern PATTERN_TAG_GROUP = Pattern.compile("<tr><td[^<>]+>([\\w\\s]+):</td><td>(?:<div[^<>]+><a[^<>]+>[\\w\\s]+</a></div>)+</td></tr>");
private static final Pattern PATTERN_TAG = Pattern.compile("<div[^<>]+><a[^<>]+>([\\w\\s]+)</a></div>");
private static final Pattern PATTERN_COMMENT = Pattern.compile("<div class=\"c3\">Posted on ([^<>]+) by: &nbsp; <a[^<>]+>([^<>]+)</a>.+?<div class=\"c6\"[^>]*>(.+?)</div><div class=\"c[78]\"");
private static final Pattern PATTERN_PAGES = Pattern.compile("<tr><td[^<>]*>Length:</td><td[^<>]*>([\\d,]+) pages</td></tr>");
private static final Pattern PATTERN_PREVIEW_PAGES = Pattern.compile("<td[^>]+><a[^>]+>([\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>");
private static final Pattern PATTERN_NORMAL_PREVIEW = Pattern.compile("<div class=\"gdtm\"[^<>]*><div[^<>]*width:(\\d+)[^<>]*height:(\\d+)[^<>]*\\((.+?)\\)[^<>]*-(\\d+)px[^<>]*><a[^<>]*href=\"(.+?)\"[^<>]*><img alt=\"([\\d,]+)\"");
@@ -76,16 +71,13 @@ public class GalleryDetailParser {
private static final GalleryTagGroup[] EMPTY_GALLERY_TAG_GROUP_ARRAY = new GalleryTagGroup[0];
private static final GalleryCommentList EMPTY_GALLERY_COMMENT_ARRAY = new GalleryCommentList(new GalleryComment[0], false);
private static final DateFormat WEB_COMMENT_DATE_FORMAT = new SimpleDateFormat("dd MMMMM yyyy, HH:mm", Locale.US);
private static final DateTimeFormatter WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC);
private static final String OFFENSIVE_STRING =
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>";
private static final String PINING_STRING =
"<p>This gallery is pining for the fjords.</p>";
static {
WEB_COMMENT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public static GalleryDetail parse(String body) throws EhException {
if (body.contains(OFFENSIVE_STRING)) {
throw new OffensiveException();
@@ -186,7 +178,7 @@ public class GalleryDetailParser {
// Uploader
Element gdn = gm.getElementById("gdn");
if (null != gdn) {
gd.disowned = "opacity:0.5".equals(gdn.attr("style"));
gd.disowned = gdn.attr("style").contains("opacity:0.5");
gd.uploader = StringUtils.trim(gdn.text());
} else {
gd.uploader = "";
@@ -196,14 +188,13 @@ public class GalleryDetailParser {
gd.posted = "";
gd.parent = "";
gd.visible = "";
gd.visible = "";
gd.size = "";
gd.pages = 0;
gd.favoriteCount = 0;
try {
Elements es = gdd.child(0).child(0).children();
for (int i = 0, n = es.size(); i < n; i++) {
parseDetailInfo(gd, es.get(i), body);
parseDetailInfo(gd, es.get(i));
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -291,7 +282,7 @@ public class GalleryDetailParser {
}
}
private static void parseDetailInfo(GalleryDetail gd, Element e, String body) {
private static void parseDetailInfo(GalleryDetail gd, Element e) {
Elements es = e.children();
if (es.size() < 2) {
return;
@@ -321,20 +312,16 @@ public class GalleryDetailParser {
}
} else if (key.startsWith("Favorited")) {
switch (value) {
case "Never":
gd.favoriteCount = 0;
break;
case "Once":
gd.favoriteCount = 1;
break;
default:
case "Never" -> gd.favoriteCount = 0;
case "Once" -> gd.favoriteCount = 1;
default -> {
int index = value.indexOf(' ');
if (index == -1) {
gd.favoriteCount = 0;
} else {
gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0);
}
break;
}
}
}
}
@@ -375,6 +362,7 @@ public class GalleryDetailParser {
public static GalleryTagGroup[] parseTagGroups(Document document) {
try {
Element taglist = document.getElementById("taglist");
assert taglist != null;
Elements tagGroups = taglist.child(0).child(0).children();
return parseTagGroups(tagGroups);
} catch (Throwable e) {
@@ -394,7 +382,7 @@ public class GalleryDetailParser {
list.add(group);
}
}
return list.toArray(new GalleryTagGroup[list.size()]);
return list.toArray(new GalleryTagGroup[0]);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -402,31 +390,6 @@ public class GalleryDetailParser {
}
}
/**
* Parse tag groups with regular expressions
*/
@NonNull
private static GalleryTagGroup[] parseTagGroups(String body) throws EhException {
List<GalleryTagGroup> list = new LinkedList<>();
Matcher m = PATTERN_TAG_GROUP.matcher(body);
while (m.find()) {
GalleryTagGroup tagGroup = new GalleryTagGroup();
tagGroup.groupName = ParserUtils.trim(m.group(1));
parseGroup(tagGroup, m.group(0));
list.add(tagGroup);
}
return list.toArray(new GalleryTagGroup[list.size()]);
}
private static void parseGroup(GalleryTagGroup tagGroup, String body) {
Matcher m = PATTERN_TAG.matcher(body);
while (m.find()) {
tagGroup.addTag(ParserUtils.trim(m.group(1)));
}
}
@Nullable
@SuppressWarnings("ConstantConditions")
public static GalleryComment parseComment(Element element) {
@@ -444,17 +407,15 @@ public class GalleryDetailParser {
}
for (Element e : c4.children()) {
switch (e.text()) {
case "Vote+":
case "Vote+" -> {
comment.voteUpAble = true;
comment.voteUpEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Vote-":
}
case "Vote-" -> {
comment.voteDownAble = true;
comment.voteDownEd = !StringUtils.trim(e.attr("style")).isEmpty();
break;
case "Edit":
comment.editable = true;
break;
}
case "Edit" -> comment.editable = true;
}
}
}
@@ -475,7 +436,7 @@ public class GalleryDetailParser {
Element c3 = JsoupUtils.getElementByClass(element, "c3");
String temp = c3.ownText();
temp = temp.substring("Posted on ".length(), temp.length() - " by:".length());
comment.time = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
// user
comment.user = c3.child(0).text();
// comment
@@ -485,7 +446,7 @@ public class GalleryDetailParser {
if (c8 != null) {
Element e = c8.children().first();
if (e != null) {
comment.lastEdited = WEB_COMMENT_DATE_FORMAT.parse(temp).getTime();
comment.lastEdited = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(temp)).toEpochMilli();
}
}
return comment;
@@ -503,6 +464,7 @@ public class GalleryDetailParser {
public static GalleryCommentList parseComments(Document document) {
try {
Element cdiv = document.getElementById("cdiv");
assert cdiv != null;
Elements c1s = cdiv.getElementsByClass("c1");
List<GalleryComment> list = new ArrayList<>(c1s.size());
@@ -515,20 +477,21 @@ public class GalleryDetailParser {
Element chd = cdiv.getElementById("chd");
MutableBoolean hasMore = new MutableBoolean(false);
assert chd != null;
NodeTraversor.traverse(new NodeVisitor() {
@Override
public void head(Node node, int depth) {
public void head(@NonNull Node node, int depth) {
if (node instanceof Element && ((Element) node).text().equals("click to show all")) {
hasMore.value = true;
}
}
@Override
public void tail(Node node, int depth) {
public void tail(@NonNull Node node, int depth) {
}
}, chd);
return new GalleryCommentList(list.toArray(new GalleryComment[list.size()]), hasMore.value);
return new GalleryCommentList(list.toArray(new GalleryComment[0]), hasMore.value);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
e.printStackTrace();
@@ -536,38 +499,14 @@ public class GalleryDetailParser {
}
}
/**
* Parse comments with regular expressions
*/
@NonNull
public static GalleryComment[] parseComments(String body) {
List<GalleryComment> list = new LinkedList<>();
Matcher m = PATTERN_COMMENT.matcher(body);
while (m.find()) {
String webDateString = ParserUtils.trim(m.group(1));
Date date;
try {
date = WEB_COMMENT_DATE_FORMAT.parse(webDateString);
} catch (java.text.ParseException e) {
date = new Date(0L);
}
GalleryComment comment = new GalleryComment();
comment.time = date.getTime();
comment.user = ParserUtils.trim(m.group(2));
comment.comment = m.group(3);
list.add(comment);
}
return list.toArray(new GalleryComment[list.size()]);
}
/**
* Parse preview pages with html parser
*/
public static int parsePreviewPages(Document document, String body) throws ParseException {
try {
Elements elements = document.getElementsByClass("ptt").first().child(0).child(0).children();
Element ptt = document.getElementsByClass("ptt").first();
assert ptt != null;
Elements elements = ptt.child(0).child(0).children();
return Integer.parseInt(elements.get(elements.size() - 2).text());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
@@ -634,9 +573,10 @@ public class GalleryDetailParser {
try {
LargePreviewSet largePreviewSet = new LargePreviewSet();
Element gdt = d.getElementById("gdt");
assert gdt != null;
Elements gdtls = gdt.getElementsByClass("gdtl");
int n = gdtls.size();
if (n <= 0) {
if (n == 0) {
throw new ParseException("Can't parse large preview", body);
}
for (int i = 0; i < n; i++) {
@@ -24,7 +24,6 @@ import androidx.annotation.NonNull;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.client.data.GalleryTagGroup;
import com.hippo.ehviewer.client.exception.ParseException;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.JsoupUtils;
@@ -67,16 +66,6 @@ public class GalleryListParser {
new String[]{"224", "128", "224"},
};
private static int parsePages(Document d, String body) throws ParseException {
try {
Elements es = d.getElementsByClass("ptt").first().child(0).child(0).children();
return Integer.parseInt(es.get(es.size() - 2).text().trim());
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
throw new ParseException("Can't parse gallery list pages", body);
}
}
private static String parseRating(String ratingStyle) {
Matcher m = PATTERN_RATING.matcher(ratingStyle);
int num1 = Integer.MIN_VALUE;
@@ -147,23 +136,21 @@ public class GalleryListParser {
children = child.children();
}
gi.title = child.text().trim();
Element tbody = JsoupUtils.getElementByTag(glname, "tbody");
if (tbody != null) {
ArrayList<String> tags = new ArrayList<>();
GalleryTagGroup[] groups = GalleryDetailParser.parseTagGroups(tbody.children());
for (GalleryTagGroup group : groups) {
for (int j = 0; j < group.size(); j++) {
tags.add(group.groupName + ":" + group.getTagAt(j));
}
}
gi.simpleTags = tags.toArray(new String[tags.size()]);
}
}
if (gi.title == null) {
return null;
}
// Tags
Elements gts = e.select(".gt, .gtl");
if (gts.size() != 0) {
ArrayList<String> tags = new ArrayList<>();
for (Element gt : gts) {
tags.add(gt.attr("title"));
}
gi.simpleTags = tags.toArray(new String[0]);
}
// Category
gi.category = EhUtils.UNKNOWN;
Element ce = JsoupUtils.getElementByClass(e, "cn");
@@ -267,10 +254,12 @@ public class GalleryListParser {
if (children.size() > uploaderIndex) {
Element div = children.get(uploaderIndex);
if (div != null) {
gi.disowned = "opacity:0.5".equals(div.attr("style"));
gi.disowned = div.attr("style").contains("opacity:0.5");
Element a = div.children().first();
if (a != null) {
gi.uploader = a.text().trim();
} else {
gi.uploader = div.text().trim();
}
}
}
@@ -19,17 +19,18 @@ package com.hippo.ehviewer.client.parser;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class ParserUtils {
public static final DateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
public static final DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC);
public static synchronized String formatDate(long time) {
return sDateFormat.format(new Date(time));
return formatter.format(Instant.ofEpochMilli(time));
}
public static String trim(String str) {
@@ -48,7 +49,4 @@ public class ParserUtils {
return NumberUtils.parseLongSafely(trim(str).replace(",", ""), defValue);
}
public static float parseFloat(String str, float defValue) {
return NumberUtils.parseFloatSafely(trim(str).replace(",", ""), defValue);
}
}
@@ -67,7 +67,7 @@ class DownloadService : Service(), DownloadManager.DownloadListener {
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.getDownloadManager(applicationContext)
mDownloadManager = EhApplication.downloadManager
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
@@ -17,9 +17,9 @@
package com.hippo.ehviewer.gallery;
import android.content.Context;
import android.graphics.ImageDecoder;
import android.net.Uri;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import androidx.annotation.NonNull;
@@ -36,7 +36,6 @@ import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -335,10 +334,15 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
Image image = null;
if (buffer != null) {
image = Image.decode(buffer, false, () -> {
try {
image = Image.decode(buffer, false, () -> {
archiveAccessor.releaseByteBuffer(buffer);
return null;
});
} catch (ImageDecoder.DecodeException e) {
archiveAccessor.releaseByteBuffer(buffer);
return null;
});
e.printStackTrace();
}
}
if (image != null) {
notifyPageSucceed(index, image);
@@ -261,11 +261,7 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable {
is = files[index].openInputStream();
Image image = Image.decode((FileInputStream) is, false);
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
notifyPageSucceed(index, image);
} catch (IOException e) {
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_reading_failed));
@@ -40,7 +40,7 @@ class AccountPreference @JvmOverloads constructor(
private var message: String? = null
init {
val store = EhApplication.getEhCookieStore(context)
val store = EhApplication.ehCookieStore
val eCookies = store.getCookies(EhUrl.HOST_E.toHttpUrl())
val exCookies = store.getCookies(EhUrl.HOST_EX.toHttpUrl())
val cookies: MutableList<Cookie> = LinkedList(eCookies)
@@ -55,7 +55,7 @@ public class CleanRedundancyPreference extends TaskPreference {
public ClearTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager(context.getApplicationContext());
mManager = EhApplication.getDownloadManager();
}
// True for cleared
@@ -161,7 +161,7 @@ class ProxyPreference @JvmOverloads constructor(
Settings.putProxyIp(ip)
Settings.putProxyPort(port)
updateSummary(type, ip, port)
EhApplication.getEhProxySelector(getContext()).updateProxy()
EhApplication.ehProxySelector.updateProxy()
dialog.dismiss()
}
}
@@ -74,8 +74,8 @@ public class RestoreDownloadPreference extends TaskPreference {
public RestoreTask(@NonNull Context context) {
super(context);
mManager = EhApplication.getDownloadManager(context.getApplicationContext());
mHttpClient = EhApplication.getOkHttpClient(context.getApplicationContext());
mManager = EhApplication.getDownloadManager();
mHttpClient = EhApplication.getOkHttpClient();
}
private RestoreItem getRestoreItem(UniFile file) {
@@ -28,7 +28,6 @@ import com.hippo.ehviewer.client.EhCookieStore;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.ui.SettingsActivity;
import com.hippo.ehviewer.ui.scene.BaseScene;
import com.hippo.util.WebViewUtil;
import okhttp3.HttpUrl;
@@ -59,11 +58,7 @@ public class SignInRequiredPreference extends Preference implements Preference.O
@Override
public boolean onPreferenceClick(Preference preference) {
if (!WebViewUtil.available(getContext())) {
mActivity.showTip(R.string.error_no_webview, BaseScene.LENGTH_SHORT);
return true;
}
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
EhCookieStore store = EhApplication.getEhCookieStore();
HttpUrl e = HttpUrl.get(EhUrl.HOST_E);
HttpUrl ex = HttpUrl.get(EhUrl.HOST_EX);
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.spider;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.AsyncTask;
import android.os.Process;
import android.text.TextUtils;
@@ -157,8 +158,8 @@ public final class SpiderQueen implements Runnable {
private volatile int mDownloadPage = -1;
private SpiderQueen(EhApplication application, @NonNull GalleryInfo galleryInfo) {
mHttpClient = EhApplication.getOkHttpClient(application);
mSpiderInfoCache = EhApplication.getSpiderInfoCache(application);
mHttpClient = EhApplication.getOkHttpClient();
mSpiderInfoCache = EhApplication.getSpiderInfoCache();
mGalleryInfo = galleryInfo;
mSpiderDen = new SpiderDen(mGalleryInfo);
@@ -1679,6 +1680,8 @@ public final class SpiderQueen implements Runnable {
if (is != null) {
try {
image = Image.decode(((FileInputStream) is), false);
} catch (ImageDecoder.DecodeException e) {
e.printStackTrace();
} finally {
try {
is.close();
@@ -53,7 +53,7 @@ public final class CommonOperations {
EhDB.putLocalFavorites(galleryInfo);
listener.onSuccess(null);
} else if (slot >= 0 && slot <= 9) {
EhClient client = EhApplication.getEhClient(activity);
EhClient client = EhApplication.getEhClient();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
request.setArgs(galleryInfo.gid, galleryInfo.token, slot, "");
@@ -100,7 +100,7 @@ public final class CommonOperations {
public static void removeFromFavorites(Activity activity, GalleryInfo galleryInfo,
final EhClient.Callback<Void> listener) {
EhDB.removeLocalFavorites(galleryInfo.gid);
EhClient client = EhApplication.getEhClient(activity);
EhClient client = EhApplication.getEhClient();
EhRequest request = new EhRequest();
request.setMethod(EhClient.METHOD_ADD_FAVORITES);
request.setArgs(galleryInfo.gid, galleryInfo.token, -1, "");
@@ -114,13 +114,13 @@ public final class CommonOperations {
public static void startDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Objects.requireNonNull(EhApplication.getInstance().getTopActivity()).checkAndRequestNotificationPermission();
Objects.requireNonNull(EhApplication.getApplication().getTopActivity()).checkAndRequestNotificationPermission();
}
doStartDownload(activity, galleryInfos, forceDefault);
}
private static void doStartDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
final DownloadManager dm = EhApplication.getDownloadManager(activity);
final DownloadManager dm = EhApplication.getDownloadManager();
LongList toStart = new LongList();
List<GalleryInfo> toAdd = new ArrayList<>();
@@ -22,7 +22,6 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.content.res.Resources.Theme
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
@@ -32,7 +31,6 @@ import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.EhApplication.locked_last_leave_time
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import rikka.core.res.resolveColor
@@ -71,23 +69,15 @@ abstract class EhActivity : AppCompatActivity() {
WindowInsetsHelper.LISTENER
)
super.onCreate(savedInstanceState)
window.statusBarColor = Color.TRANSPARENT
window.decorView.post {
val rootWindowInsets = window.decorView.rootWindowInsets
if (rootWindowInsets != null && rootWindowInsets.systemWindowInsetBottom >= Resources.getSystem().displayMetrics.density * 40) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
}
window.navigationBarDividerColor = getColor(R.color.navigation_bar_divider)
window.navigationBarColor =
theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
} else {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = true
}
}
}
(application as EhApplication).registerActivity(this)
@@ -100,7 +90,7 @@ abstract class EhActivity : AppCompatActivity() {
override fun onResume() {
val locked_resume_time = System.currentTimeMillis() / 1000
val locked_delay_time = locked_resume_time - locked_last_leave_time
val locked_delay_time = locked_resume_time - EhApplication.locked_last_leave_time
if (locked_delay_time < Settings.getSecurityDelay() * 60) {
EhApplication.locked = false
} else if (Settings.getSecurity() && isAuthenticationSupported() && EhApplication.locked) {
@@ -511,6 +511,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
mSeekBar = null;
SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable);
}
@Override
protected void onPause() {
super.onPause();
if (mGLRootView != null) {
mGLRootView.onPause();
}
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
@@ -522,15 +531,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
mSavingPage = -1;
});
@Override
protected void onPause() {
super.onPause();
if (mGLRootView != null) {
mGLRootView.onPause();
}
}
@Override
protected void onResume() {
super.onResume();
@@ -15,12 +15,10 @@
*/
package com.hippo.ehviewer.ui
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -46,13 +44,6 @@ class GalleryInfoBottomSheet(detail: GalleryDetail) : BottomSheetDialogFragment(
): View? {
parseDetail()
val view = inflater.inflate(R.layout.scene_gallery_info, container, false)
dialog?.window?.run {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
attributes.blurBehindRadius = 32
}
}
val recyclerView = view.findViewById(R.id.recycler_view) as RecyclerView
val adapter = InfoAdapter()
recyclerView.adapter = adapter
@@ -45,39 +45,68 @@ class SecurityActivity : AppCompatActivity() {
onSuccess()
}
@Synchronized
private fun startAuthentication(
title: String,
subtitle: String? = null,
confirmationRequired: Boolean = true,
) {
if (isAuthenticating) return
isAuthenticating = true
startClass2BiometricOrCredentialAuthentication(
title = title,
subtitle = subtitle,
confirmationRequired = confirmationRequired,
executor = ContextCompat.getMainExecutor(this),
callback = AuthenticationCallback()
callback = callback(
{ _, _, _ -> onFailed() },
{ _, _ -> onSuccess() },
{ onFailed() }
)
)
}
inner class AuthenticationCallback : AuthPromptCallback() {
override fun onAuthenticationSucceeded(
inline fun callback(
crossinline onAuthenticationError: (
activity: FragmentActivity?,
errorCode: Int,
errString: CharSequence
) -> Unit,
crossinline onAuthenticationSucceeded: (
activity: FragmentActivity?,
result: BiometricPrompt.AuthenticationResult
) {
onSuccess()
}
) -> Unit,
crossinline onAuthenticationFailed: (
activity: FragmentActivity?
) -> Unit
) = object : AuthPromptCallback() {
override fun onAuthenticationError(
activity: FragmentActivity?,
errorCode: Int,
errString: CharSequence
) {
moveTaskToBack(true)
}
) = onAuthenticationError(activity, errorCode, errString)
override fun onAuthenticationSucceeded(
activity: FragmentActivity?,
result: BiometricPrompt.AuthenticationResult
) = onAuthenticationSucceeded(activity, result)
override fun onAuthenticationFailed(activity: FragmentActivity?) =
onAuthenticationFailed(activity)
}
private fun onSuccess() {
isAuthenticating = false
EhApplication.locked = false
finish()
}
private fun onFailed() {
moveTaskToBack(true)
isAuthenticating = false
}
companion object {
private var isAuthenticating = false
}
}
@@ -305,7 +305,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
}
private void backupFavorite() {
EhClient mClient = EhApplication.getEhClient(requireContext());
EhClient mClient = EhApplication.getEhClient();
FavListUrlBuilder favListUrlBuilder = new FavListUrlBuilder();
favTotal = 0;
favIndex = 1;
@@ -20,6 +20,7 @@ import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.Preference
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhTagDatabase
@@ -78,7 +79,7 @@ class EhFragment : BasePreferenceFragment() {
}
} else if (Settings.KEY_BLACK_DARK_THEME == key) {
if (requireActivity().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0) {
requireActivity().recreate()
EhApplication.application.recreateAllActivity()
}
return true
}
@@ -43,7 +43,7 @@ class HostsFragment : BaseFragment(), View.OnClickListener {
private var adapter: HostsAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EhApplication.getHosts(requireContext()).apply {
EhApplication.hosts.apply {
hosts = this
data = all
}
@@ -84,7 +84,7 @@ public class MyTagsFragment extends BaseFragment {
// Copy cookies from okhttp cookie store to CookieManager
url = EhUrl.getMyTagsUrl();
EhCookieStore store = EhApplication.getEhCookieStore(requireContext());
EhCookieStore store = EhApplication.getEhCookieStore();
for (Cookie cookie : store.getCookies(HttpUrl.parse(url))) {
cookieManager.setCookie(url, cookie.toString());
}
@@ -91,7 +91,7 @@ public class UConfigFragment extends BaseFragment {
// Copy cookies from okhttp cookie store to CookieManager
url = EhUrl.getUConfigUrl();
EhCookieStore store = EhApplication.getEhCookieStore(requireContext());
EhCookieStore store = EhApplication.getEhCookieStore();
for (Cookie cookie : store.getCookies(HttpUrl.parse(url))) {
cookieManager.setCookie(url, cookie.toString());
}
@@ -142,7 +142,7 @@ public class UConfigFragment extends BaseFragment {
String cookiesString = cookieManager.getCookie(url);
if (cookiesString != null && !cookiesString.isEmpty()) {
EhCookieStore store = EhApplication.getEhCookieStore(requireContext());
EhCookieStore store = EhApplication.getEhCookieStore();
HttpUrl eUrl = HttpUrl.parse(EhUrl.HOST_E);
HttpUrl exUrl = HttpUrl.parse(EhUrl.HOST_EX);
if (eUrl == null || exUrl == null) {
@@ -240,7 +240,7 @@ public class CookieSignInScene extends SolidScene implements EditText.OnEditorAc
EhUtils.signOut(context);
EhCookieStore store = EhApplication.getEhCookieStore(context);
EhCookieStore store = EhApplication.getEhCookieStore();
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_E));
store.addCookie(newCookie(EhCookieStore.KEY_IPD_MEMBER_ID, id, EhUrl.DOMAIN_EX));
store.addCookie(newCookie(EhCookieStore.KEY_IPD_PASS_HASH, hash, EhUrl.DOMAIN_E));
@@ -162,7 +162,7 @@ public class DownloadsScene extends ToolbarScene
if (context == null) {
return;
}
List<DownloadLabel> listLabel = EhApplication.getDownloadManager(context).getLabelList();
List<DownloadLabel> listLabel = EhApplication.getDownloadManager().getLabelList();
mLabels = new ArrayList<>(listLabel.size() + 1);
// Add default label name
mLabels.add(getString(R.string.default_download_label_name));
@@ -214,7 +214,7 @@ public class DownloadsScene extends ToolbarScene
Context context = getContext();
AssertUtils.assertNotNull(context);
mDownloadManager = EhApplication.getDownloadManager(context);
mDownloadManager = EhApplication.getDownloadManager();
mDownloadManager.addDownloadInfoListener(this);
if (savedInstanceState == null) {
@@ -233,7 +233,7 @@ public class DownloadsScene extends ToolbarScene
if (null == manager) {
Context context = getContext();
if (null != context) {
manager = EhApplication.getDownloadManager(context);
manager = EhApplication.getDownloadManager();
}
} else {
mDownloadManager = null;
@@ -314,6 +314,11 @@ public class DownloadsScene extends ToolbarScene
setLiftOnScrollTargetView(mRecyclerView);
mFastScroller = (FastScroller) ViewUtils.$$(content, R.id.fast_scroller);
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
// Workaround
((ViewGroup) mFabLayout.getParent()).removeView(mFabLayout);
container.addView(mFabLayout);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(content, mTip);
@@ -389,6 +394,7 @@ public class DownloadsScene extends ToolbarScene
}
if (null != mFabLayout) {
removeAboveSnackView(mFabLayout);
((ViewGroup) mFabLayout.getParent()).removeView(mFabLayout);
mFabLayout = null;
}
@@ -691,7 +697,7 @@ public class DownloadsScene extends ToolbarScene
break;
}
case 4: {// Move
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager(context).getLabelList();
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager().getLabelList();
List<String> labelList = new ArrayList<>(labelRawList.size() + 1);
labelList.add(getString(R.string.default_download_label_name));
for (int i = 0, n = labelRawList.size(); i < n; i++) {
@@ -900,7 +906,7 @@ public class DownloadsScene extends ToolbarScene
String label = mLabels.get(position);
if (mDownloadManager == null) {
if (context != null) {
mDownloadManager = EhApplication.getDownloadManager(context);
mDownloadManager = EhApplication.getDownloadManager();
}
}
List<DownloadInfo> list = null;
@@ -1034,7 +1040,7 @@ public class DownloadsScene extends ToolbarScene
} else {
label = mLabels[which];
}
EhApplication.getDownloadManager(context).changeLabel(mDownloadInfoList, label);
EhApplication.getDownloadManager().changeLabel(mDownloadInfoList, label);
if (mLabelAdapter != null) {
mLabelAdapter.notifyDataSetChanged();
}
@@ -1156,7 +1162,7 @@ public class DownloadsScene extends ToolbarScene
return;
}
DownloadInfo info = mList.get(position);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb, true);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(info.gid), info.thumb);
holder.title.setText(EhUtils.getSuitableTitle(info));
holder.uploader.setText(info.uploader);
holder.rating.setRating(info.rating);
@@ -1240,12 +1246,12 @@ public class DownloadsScene extends ToolbarScene
mBuilder.setError(getString(R.string.label_text_is_empty));
} else if (getString(R.string.default_download_label_name).equals(text)) {
mBuilder.setError(getString(R.string.label_text_is_invalid));
} else if (EhApplication.getDownloadManager(context).containLabel(text)) {
} else if (EhApplication.getDownloadManager().containLabel(text)) {
mBuilder.setError(getString(R.string.label_text_exist));
} else {
mBuilder.setError(null);
mDialog.dismiss();
EhApplication.getDownloadManager(context).renameLabel(mOriginalLabel, text);
EhApplication.getDownloadManager().renameLabel(mOriginalLabel, text);
if (mLabelAdapter != null) {
initLabels();
mLabelAdapter.notifyDataSetChanged();
@@ -1280,12 +1286,12 @@ public class DownloadsScene extends ToolbarScene
mBuilder.setError(getString(R.string.label_text_is_empty));
} else if (getString(R.string.default_download_label_name).equals(text)) {
mBuilder.setError(getString(R.string.label_text_is_invalid));
} else if (EhApplication.getDownloadManager(context).containLabel(text)) {
} else if (EhApplication.getDownloadManager().containLabel(text)) {
mBuilder.setError(getString(R.string.label_text_exist));
} else {
mBuilder.setError(null);
mDialog.dismiss();
EhApplication.getDownloadManager(context).addLabel(text);
EhApplication.getDownloadManager().addLabel(text);
initLabels();
if (mLabelAdapter != null && mLabels != null) {
mLabelAdapter.notifyItemInserted(mLabels.size() - 1);
@@ -1311,7 +1317,7 @@ public class DownloadsScene extends ToolbarScene
return false;
}
EhApplication.getDownloadManager(context).moveLabel(fromPosition - 1, toPosition - 1);
EhApplication.getDownloadManager().moveLabel(fromPosition - 1, toPosition - 1);
final String item = mLabels.remove(fromPosition);
mLabels.add(toPosition, item);
mLabelAdapter.notifyDataSetChanged();
@@ -34,7 +34,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.drawerlayout.widget.DrawerLayout;
@@ -47,10 +46,8 @@ import com.google.android.material.datepicker.DateValidatorPointBackward;
import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.annotation.Implemented;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.ehviewer.EhApplication;
@@ -70,12 +67,10 @@ import com.hippo.ehviewer.ui.annotation.DrawerLifeCircle;
import com.hippo.ehviewer.ui.annotation.ViewLifeCircle;
import com.hippo.ehviewer.ui.annotation.WholeLifeCircle;
import com.hippo.ehviewer.widget.GalleryInfoContentHelper;
import com.hippo.ehviewer.widget.SearchBar;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.SimpleHandler;
@@ -92,8 +87,8 @@ import rikka.core.res.ResourcesKt;
// TODO Get favorite, modify favorite, add favorite, what a mess!
@SuppressLint("RtlHardcoded")
public class FavoritesScene extends BaseScene implements
FastScroller.OnDragHandlerListener, SearchBarMover.Helper, SearchBar.Helper,
public class FavoritesScene extends SearchBarScene implements
FastScroller.OnDragHandlerListener,
FabLayout.OnClickFabListener, FabLayout.OnExpandListener,
EasyRecyclerView.CustomChoiceListener {
@@ -114,9 +109,6 @@ public class FavoritesScene extends BaseScene implements
private EasyRecyclerView mRecyclerView;
@Nullable
@ViewLifeCircle
private SearchBar mSearchBar;
@Nullable
@ViewLifeCircle
private FabLayout mFabLayout;
@Nullable
@ViewLifeCircle
@@ -124,12 +116,6 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@ViewLifeCircle
private FavoritesHelper mHelper;
@Nullable
@ViewLifeCircle
private SearchBarMover mSearchBarMover;
@Nullable
@ViewLifeCircle
private DrawerArrowDrawable mLeftDrawable;
private AddDeleteDrawable mActionFabDrawable;
@Nullable
private DrawerLayout mDrawerLayout;
@@ -189,7 +175,7 @@ public class FavoritesScene extends BaseScene implements
Context context = getContext();
AssertUtils.assertNotNull(context);
mClient = EhApplication.getEhClient(context);
mClient = EhApplication.getEhClient();
mFavCatArray = Settings.getFavCat();
mFavCountArray = Settings.getFavCount();
mFavLocalCount = Settings.getFavLocalCount();
@@ -247,17 +233,22 @@ public class FavoritesScene extends BaseScene implements
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
public View onCreateViewWithToolbar(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_favorites, container, false);
mContentLayout = view.findViewById(R.id.content_layout);
MainActivity activity = getMainActivity();
AssertUtils.assertNotNull(activity);
setOnApplySearch((query) -> {
onApplySearch(query);
return null;
});
mDrawerLayout = (DrawerLayout) ViewUtils.$$(activity, R.id.draw_view);
mRecyclerView = mContentLayout.getRecyclerView();
FastScroller fastScroller = mContentLayout.getFastScroller();
mSearchBar = (SearchBar) ViewUtils.$$(view, R.id.search_bar);
mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout);
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
container.addView(mFabLayout);
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mFabLayout
@@ -283,14 +274,9 @@ public class FavoritesScene extends BaseScene implements
fastScroller.setPadding(fastScroller.getPaddingLeft(), fastScroller.getPaddingTop() + paddingTopSB,
fastScroller.getPaddingRight(), fastScroller.getPaddingBottom());
mLeftDrawable = new DrawerArrowDrawable(context, ResourcesKt.resolveColor(getTheme(), android.R.attr.colorControlNormal));
mSearchBar.setLeftDrawable(mLeftDrawable);
mSearchBar.setRightDrawable(ContextCompat.getDrawable(context, R.drawable.v_magnify_x24));
mSearchBar.setHelper(this);
mSearchBar.setAllowEmptySearch(false);
setAllowEmptySearch(false);
updateSearchBar();
updateJumpFab();
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView);
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
@@ -321,7 +307,7 @@ public class FavoritesScene extends BaseScene implements
// They changed, call it
private void updateSearchBar() {
Context context = getContext();
if (null == context || null == mUrlBuilder || null == mSearchBar || null == mFavCatArray) {
if (null == context || null == mUrlBuilder || null == mFavCatArray) {
return;
}
@@ -338,17 +324,17 @@ public class FavoritesScene extends BaseScene implements
String keyword = mUrlBuilder.getKeyword();
if (TextUtils.isEmpty(keyword)) {
if (!ObjectUtils.equal(favCatName, mOldFavCat)) {
mSearchBar.setTitle(getString(R.string.favorites_title, favCatName));
setSearchBarHint(getString(R.string.favorites_title, favCatName));
}
} else {
if (!ObjectUtils.equal(favCatName, mOldFavCat) || !ObjectUtils.equal(keyword, mOldKeyword)) {
mSearchBar.setTitle(getString(R.string.favorites_title_2, favCatName, keyword));
setSearchBarHint(getString(R.string.favorites_title_2, favCatName, keyword));
}
}
// Update hint
if (!ObjectUtils.equal(favCatName, mOldFavCat)) {
mSearchBar.setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName));
setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName));
}
mOldFavCat = favCatName;
@@ -380,17 +366,13 @@ public class FavoritesScene extends BaseScene implements
mRecyclerView = null;
}
if (null != mFabLayout) {
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
removeAboveSnackView(mFabLayout);
mFabLayout = null;
}
mAdapter = null;
mSearchBar = null;
mSearchBarMover = null;
mLeftDrawable = null;
mOldFavCat = null;
mOldKeyword = null;
}
@@ -446,29 +428,27 @@ public class FavoritesScene extends BaseScene implements
mFabLayout.toggle();
} else if (mSearchMode) {
exitSearchMode(true);
} else if (isSearchViewShown()) {
hideSearchView();
} else {
finish();
}
}
@Override
@Implemented(FastScroller.OnDragHandlerListener.class)
public void onStartDragHandler() {
// Lock right drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.RIGHT);
}
@Override
@Implemented(FastScroller.OnDragHandlerListener.class)
public void onEndDragHandler() {
// Restore right drawer
if (null != mRecyclerView && !mRecyclerView.isInCustomChoice()) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
}
if (mSearchBarMover != null) {
mSearchBarMover.returnSearchBarPosition();
}
showSearchBar();
}
public boolean onItemClick(View view, int position) {
@@ -534,78 +514,6 @@ public class FavoritesScene extends BaseScene implements
return true;
}
@Override
@Implemented(SearchBarMover.Helper.class)
public boolean isValidView(RecyclerView recyclerView) {
return recyclerView == mRecyclerView;
}
@Override
@Implemented(SearchBarMover.Helper.class)
public RecyclerView getValidRecyclerView() {
return mRecyclerView;
}
@Override
@Implemented(SearchBarMover.Helper.class)
public boolean forceShowSearchBar() {
return false;
}
@Override
@Implemented(SearchBar.Helper.class)
public void onClickTitle() {
// Skip if in search mode
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
return;
}
if (!mSearchMode) {
enterSearchMode(true);
}
}
@Override
@Implemented(SearchBar.Helper.class)
public void onClickLeftIcon() {
// Skip if in search mode
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
return;
}
if (mSearchMode) {
exitSearchMode(true);
} else {
toggleDrawer(Gravity.LEFT);
}
}
@Override
@Implemented(SearchBar.Helper.class)
public void onClickRightIcon() {
// Skip if in search mode
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
return;
}
if (!mSearchMode) {
enterSearchMode(true);
} else if (mSearchBar != null) {
if (mSearchBar.getEditText().length() == 0) {
exitSearchMode(true);
} else {
mSearchBar.applySearch();
}
}
}
@Override
@Implemented(SearchBar.Helper.class)
public void onSearchEditTextClick() {
}
@Override
@Implemented(SearchBar.Helper.class)
public void onApplySearch(String query) {
// Skip if in search mode
if (mRecyclerView != null && mRecyclerView.isInCustomChoice()) {
@@ -628,12 +536,6 @@ public class FavoritesScene extends BaseScene implements
mHelper.refresh();
}
@Override
@Implemented(SearchBar.Helper.class)
public void onSearchEditTextBackPressed() {
onBackPressed();
}
@Override
public void onExpand(boolean expanded) {
if (expanded) {
@@ -644,7 +546,6 @@ public class FavoritesScene extends BaseScene implements
}
@Override
@Implemented(FabLayout.OnClickFabListener.class)
public void onClickPrimaryFab(FabLayout view, FloatingActionButton fab) {
if (mRecyclerView != null && mFabLayout != null) {
if (mRecyclerView.isInCustomChoice()) {
@@ -686,7 +587,6 @@ public class FavoritesScene extends BaseScene implements
}
@Override
@Implemented(FabLayout.OnClickFabListener.class)
public void onClickSecondaryFab(FabLayout view, FloatingActionButton fab, int position) {
Context context = getContext();
if (null == context || null == mRecyclerView || null == mHelper) {
@@ -785,7 +685,6 @@ public class FavoritesScene extends BaseScene implements
}
@Override
@Implemented(EasyRecyclerView.CustomChoiceListener.class)
public void onIntoCustomChoice(EasyRecyclerView view) {
if (mFabLayout != null) {
showSelectionFabs();
@@ -802,7 +701,6 @@ public class FavoritesScene extends BaseScene implements
}
@Override
@Implemented(EasyRecyclerView.CustomChoiceListener.class)
public void onOutOfCustomChoice(EasyRecyclerView view) {
if (mFabLayout != null) {
showNormalFabs();
@@ -818,7 +716,6 @@ public class FavoritesScene extends BaseScene implements
}
@Override
@Implemented(EasyRecyclerView.CustomChoiceListener.class)
public void onItemCheckedStateChanged(EasyRecyclerView view, int position, long id, boolean checked) {
if (view.getCheckedItemCount() == 0) {
@@ -827,28 +724,23 @@ public class FavoritesScene extends BaseScene implements
}
private void enterSearchMode(boolean animation) {
if (mSearchMode || mSearchBar == null || mSearchBarMover == null || mLeftDrawable == null) {
if (mSearchMode) {
return;
}
mSearchMode = true;
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition(animation);
mLeftDrawable.setArrow(ANIMATE_TIME);
showSearchBar();
}
private void exitSearchMode(boolean animation) {
if (!mSearchMode || mSearchBar == null || mSearchBarMover == null || mLeftDrawable == null) {
if (!mSearchMode) {
return;
}
mSearchMode = false;
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMover.returnSearchBarPosition();
mLeftDrawable.setMenu(ANIMATE_TIME);
showSearchBar();
}
private void onGetFavoritesSuccess(FavoritesParser.Result result, int taskId) {
if (mHelper != null && mSearchBarMover != null &&
mHelper.isCurrentTask(taskId)) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
if (mFavCatArray != null) {
System.arraycopy(result.catArray, 0, mFavCatArray, 0, 10);
@@ -895,8 +787,7 @@ public class FavoritesScene extends BaseScene implements
}
private void onGetFavoritesFailure(Exception e, int taskId) {
if (mHelper != null && mSearchBarMover != null &&
mHelper.isCurrentTask(taskId)) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
mHelper.onGetException(taskId, e);
}
}
@@ -1332,9 +1223,7 @@ public class FavoritesScene extends BaseScene implements
@Override
public void onShowView(View hiddenView, View shownView) {
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
showSearchBar();
}
@Override
@@ -1343,11 +1232,9 @@ public class FavoritesScene extends BaseScene implements
}
@Override
protected void onScrollToPosition(int postion) {
if (0 == postion) {
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
protected void onScrollToPosition(int position) {
if (0 == position) {
showSearchBar();
}
}
@@ -85,7 +85,7 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
setType(type);
mDownloadManager = EhApplication.getDownloadManager(inflater.getContext());
mDownloadManager = EhApplication.getDownloadManager();
}
private void adjustPaddings() {
@@ -126,7 +126,7 @@ abstract class GalleryAdapter extends RecyclerView.Adapter<GalleryHolder> {
break;
}
case GalleryAdapter.TYPE_GRID: {
int columnWidth = mResources.getDimensionPixelOffset(Settings.getThumbSizeResId());
int columnWidth = Settings.getThumbSize();
mLayoutManager.setColumnSize(columnWidth);
mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE);
if (null != mListDecoration) {
@@ -278,7 +278,7 @@ public final class GalleryCommentsScene extends ToolbarScene
.setArgs(mGalleryDetail.apiUid, mGalleryDetail.apiKey, mGalleryDetail.gid, mGalleryDetail.token, id, vote)
.setCallback(new VoteCommentListener(context,
activity.getStageId(), getTag()));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
}
@SuppressLint("InflateParams")
@@ -408,7 +408,7 @@ public final class GalleryCommentsScene extends ToolbarScene
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(new RefreshCommentListener(activity, activity.getStageId(), getTag()));
EhApplication.getEhClient(activity).execute(request);
EhApplication.getEhClient().execute(request);
}
}
@@ -591,7 +591,7 @@ public final class GalleryCommentsScene extends ToolbarScene
.setArgs(url, comment, mCommentId != 0 ? Long.toString(mCommentId) : null)
.setCallback(new CommentGalleryListener(context,
activity.getStageId(), getTag(), mCommentId));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
hideSoftInput();
hideEditPanel(true);
}
@@ -709,7 +709,7 @@ public final class GalleryCommentsScene extends ToolbarScene
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(new RefreshCommentListener(activity, activity.getStageId(), getTag()));
EhApplication.getEhClient(activity).execute(request);
EhApplication.getEhClient().execute(request);
}
}
}
@@ -858,8 +858,7 @@ public final class GalleryCommentsScene extends ToolbarScene
}
private CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) {
Spanned sp = Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY, new URLImageGetter(textView,
EhApplication.getConaco(context)), null);
Spanned sp = Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY, new URLImageGetter(textView), null);
SpannableStringBuilder ssb = new SpannableStringBuilder(sp);
@@ -65,7 +65,6 @@ import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.beerbelly.BeerBelly;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
@@ -116,20 +115,17 @@ import com.hippo.widget.ObservedTextView;
import com.hippo.widget.SimpleGridAutoSpanLayout;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.IntList;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import coil.Coil;
import okhttp3.HttpUrl;
import rikka.core.res.ResourcesKt;
@@ -416,6 +412,11 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
}
}
// Judging by the uploader to exclude the cooldown period
private boolean getDisowned() {
return getUploader().equals("(Disowned)");
}
// -1 for error
private int getCategory() {
if (mGalleryDetail != null) {
@@ -560,7 +561,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
if (gid != -1) {
Context context = getContext();
AssertUtils.assertNotNull(context);
mDownloadState = EhApplication.getDownloadManager(context).getDownloadState(gid);
mDownloadState = EhApplication.getDownloadManager().getDownloadState(gid);
} else {
mDownloadState = DownloadInfo.STATE_INVALID;
}
@@ -637,8 +638,12 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mNoTags = (TextView) ViewUtils.$$(mTags, R.id.no_tags);
mComments = (LinearLayout) ViewUtils.$$(belowHeader, R.id.comments);
mCommentsText = (TextView) ViewUtils.$$(mComments, R.id.comments_text);
mComments.setOnClickListener(this);
if (Settings.getShowComments()) {
mCommentsText = (TextView) ViewUtils.$$(mComments, R.id.comments_text);
mComments.setOnClickListener(this);
} else {
mComments.setVisibility(View.GONE);
}
mPreviews = ViewUtils.$$(belowHeader, R.id.previews);
mGridLayout = (SimpleGridAutoSpanLayout) ViewUtils.$$(mPreviews, R.id.grid_layout);
@@ -664,7 +669,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
adjustViewVisibility(STATE_FAILED, false);
}
EhApplication.getDownloadManager(context).addDownloadInfoListener(this);
EhApplication.getDownloadManager().addDownloadInfoListener(this);
return view;
}
@@ -675,7 +680,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
Context context = getContext();
AssertUtils.assertNotNull(context);
EhApplication.getDownloadManager(context).removeDownloadInfoListener(this);
EhApplication.getDownloadManager().removeDownloadInfoListener(this);
mTip = null;
mViewTransition = null;
@@ -738,7 +743,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
}
// Get from cache
mGalleryDetail = EhApplication.getGalleryDetailCache(context).get(gid);
mGalleryDetail = EhApplication.getGalleryDetailCache().get(gid);
if (mGalleryDetail != null) {
return true;
}
@@ -768,7 +773,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
.setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)
.setArgs(url)
.setCallback(callback);
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
return true;
}
@@ -879,7 +884,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
Resources resources = getResources();
mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb);
mThumb.load(EhCacheKeyFactory.getThumbKey(gd.gid), gd.thumb, true);
setTitle(EhUtils.getSuitableTitle(gd));
mUploader.setText(gd.uploader);
mUploader.setAlpha(gd.disowned ? .5f : 1f);
@@ -1000,7 +1005,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
ObservedTextView c = v.findViewById(R.id.comment);
c.setMaxLines(5);
c.setText(Html.fromHtml(comment.comment, Html.FROM_HTML_MODE_LEGACY,
new URLImageGetter(c, EhApplication.getConaco(context)), null));
new URLImageGetter(c), null));
v.setBackgroundColor(Color.TRANSPARENT);
}
}
@@ -1009,6 +1014,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
private void bindPreviews(GalleryDetail gd) {
LayoutInflater inflater = getLayoutInflater();
Resources resources = getResourcesOrNull();
int previewNum = Settings.getPreviewNum();
if (null == inflater || null == resources || null == mGridLayout || null == mPreviewText) {
return;
}
@@ -1018,16 +1024,16 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
if (gd.previewPages <= 0 || previewSet == null || previewSet.size() == 0) {
mPreviewText.setText(R.string.no_previews);
return;
} else if (gd.previewPages == 1 && previewSet.size() <= 60) {
} else if (gd.previewPages == 1 && previewSet.size() <= previewNum) {
mPreviewText.setText(R.string.no_more_previews);
} else {
mPreviewText.setText(R.string.more_previews);
}
int columnWidth = resources.getDimensionPixelOffset(Settings.getThumbSizeResId());
int columnWidth = Settings.getThumbSize();
mGridLayout.setColumnSize(columnWidth);
mGridLayout.setStrategy(SimpleGridAutoSpanLayout.STRATEGY_SUITABLE_SIZE);
int size = Math.min(60, previewSet.size());
int size = Math.min(previewNum, previewSet.size());
for (int i = 0; i < size; i++) {
View view = inflater.inflate(R.layout.item_gallery_preview, mGridLayout, false);
mGridLayout.addView(view);
@@ -1093,26 +1099,16 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
if (-1L == gid) {
return;
}
File temp = AppConfig.createTempFile();
if (null == temp) {
return;
}
BeerBelly beerBelly = EhApplication.getConaco(context).getBeerBelly();
OutputStream os = null;
try {
os = new FileOutputStream(temp);
if (beerBelly.pullFromDiskCache(EhCacheKeyFactory.getThumbKey(gid), os)) {
ListUrlBuilder lub = new ListUrlBuilder();
lub.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
lub.setImagePath(temp.getPath());
lub.setUseSimilarityScan(true);
GalleryListScene.startScene(this, lub);
}
} catch (FileNotFoundException e) {
// Ignore
} finally {
IOUtils.closeQuietly(os);
var path = Coil.imageLoader(context).getDiskCache().get(EhCacheKeyFactory.getThumbKey(gid)).getData();
ListUrlBuilder lub = new ListUrlBuilder();
lub.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
lub.setImagePath(path.toString());
lub.setUseSimilarityScan(true);
GalleryListScene.startScene(this, lub);
} catch (Throwable e) {
e.printStackTrace();
}
}
@@ -1130,7 +1126,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
}
} else if (mUploader == v) {
String uploader = getUploader();
if (TextUtils.isEmpty(uploader)) {
if (TextUtils.isEmpty(uploader) || getDisowned()) {
return;
}
ListUrlBuilder lub = new ListUrlBuilder();
@@ -1139,7 +1135,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
GalleryListScene.startScene(this, lub);
} else if (mCategory == v) {
int category = getCategory();
if (category == -1) {
if (category == EhUtils.NONE || category == EhUtils.PRIVATE || category == EhUtils.UNKNOWN) {
return;
}
ListUrlBuilder lub = new ListUrlBuilder();
@@ -1148,7 +1144,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
} else if (mDownload == v) {
GalleryInfo galleryInfo = getGalleryInfo();
if (galleryInfo != null) {
if (EhApplication.getDownloadManager(context).getDownloadState(galleryInfo.gid) == DownloadInfo.STATE_INVALID) {
if (EhApplication.getDownloadManager().getDownloadState(galleryInfo.gid) == DownloadInfo.STATE_INVALID) {
CommonOperations.startDownload(activity, galleryInfo, false);
} else {
CheckBoxDialogBuilder builder = new CheckBoxDialogBuilder(context,
@@ -1156,7 +1152,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
getString(R.string.download_remove_dialog_check_text),
Settings.getRemoveImageFiles());
DeleteDialogHelper helper = new DeleteDialogHelper(
EhApplication.getDownloadManager(context), galleryInfo, builder);
EhApplication.getDownloadManager(), galleryInfo, builder);
builder.setTitle(R.string.download_remove_dialog_title)
.setPositiveButton(android.R.string.ok, helper)
.show();
@@ -1424,7 +1420,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
.setArgs(mGalleryDetail.apiUid, mGalleryDetail.apiKey, mGalleryDetail.gid, mGalleryDetail.token, tag, vote)
.setCallback(new VoteTagListener(context,
activity.getStageId(), getTag()));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
}
@Override
@@ -1435,6 +1431,9 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
}
if (mUploader == v) {
if (TextUtils.isEmpty(getUploader()) || getDisowned()) {
return false;
}
showFilterUploaderDialog();
} else if (mDownload == v) {
GalleryInfo galleryInfo = getGalleryInfo();
@@ -1527,7 +1526,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
return;
}
int downloadState = EhApplication.getDownloadManager(context).getDownloadState(gid);
int downloadState = EhApplication.getDownloadManager().getDownloadState(gid);
if (downloadState == mDownloadState) {
return;
}
@@ -1645,7 +1644,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
getApplication().removeGlobalStuff(this);
// Put gallery detail to cache
EhApplication.getGalleryDetailCache(getApplication()).put(result.gid, result);
EhApplication.getGalleryDetailCache().put(result.gid, result);
// Add history
EhDB.putHistoryInfo(result);
@@ -1725,7 +1724,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
scene.onRateGallerySuccess(result);
} else {
// Update rating in cache
GalleryDetail gd = EhApplication.getGalleryDetailCache(getApplication()).get(mGid);
GalleryDetail gd = EhApplication.getGalleryDetailCache().get(mGid);
if (gd != null) {
gd.rating = result.rating;
gd.ratingCount = result.ratingCount;
@@ -1854,7 +1853,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mRequest = new EhRequest().setMethod(EhClient.METHOD_ARCHIVE_LIST)
.setArgs(url, mGid, mToken)
.setCallback(this);
EhApplication.getEhClient(context).execute(mRequest);
EhApplication.getEhClient().execute(mRequest);
} else {
bind(mArchiveList);
}
@@ -1893,7 +1892,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
request.setMethod(EhClient.METHOD_DOWNLOAD_ARCHIVE);
request.setArgs(mGalleryDetail.gid, mGalleryDetail.token, mArchiveFormParamOr, res);
request.setCallback(new DownloadArchiveListener(context, activity.getStageId(), getTag()));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
}
if (mDialog != null) {
@@ -1971,7 +1970,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mRequest = new EhRequest().setMethod(EhClient.METHOD_GET_TORRENT_LIST)
.setArgs(url, mGid, mToken)
.setCallback(this);
EhApplication.getEhClient(context).execute(mRequest);
EhApplication.getEhClient().execute(mRequest);
} else {
bind(mTorrentList);
}
@@ -2012,7 +2011,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
FileUtils.sanitizeFilename(name + ".torrent"));
r.allowScanningByMediaScanner();
r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
r.addRequestHeader("Cookie", EhApplication.getEhCookieStore(context).getCookieHeader(HttpUrl.get(url)));
r.addRequestHeader("Cookie", EhApplication.getEhCookieStore().getCookieHeader(HttpUrl.get(url)));
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (dm != null) {
try {
@@ -2107,7 +2106,7 @@ public class GalleryDetailScene extends CollapsingToolbarScene implements View.O
mGalleryDetail.gid, mGalleryDetail.token, mRatingBar.getRating())
.setCallback(new RateGalleryListener(context,
activity.getStageId(), getTag(), mGalleryDetail.gid));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
}
}
@@ -32,6 +32,7 @@ import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
@@ -64,7 +65,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.ehviewer.EhApplication;
@@ -92,7 +92,6 @@ import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter;
import com.hippo.ehviewer.widget.GalleryInfoContentHelper;
import com.hippo.ehviewer.widget.SearchBar;
import com.hippo.ehviewer.widget.SearchLayout;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
@@ -102,7 +101,6 @@ import com.hippo.view.BringOutTransition;
import com.hippo.view.ViewTransition;
import com.hippo.widget.ContentLayout;
import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.MathUtils;
@@ -123,9 +121,9 @@ import java.util.List;
import rikka.core.res.ResourcesKt;
@SuppressLint("RtlHardcoded")
public final class GalleryListScene extends BaseScene
implements SearchBar.Helper, SearchBar.OnStateChangeListener, FastScroller.OnDragHandlerListener,
SearchLayout.Helper, SearchBarMover.Helper, View.OnClickListener, FabLayout.OnClickFabListener,
public final class GalleryListScene extends SearchBarScene
implements FastScroller.OnDragHandlerListener,
SearchLayout.Helper, FabLayout.OnClickFabListener,
FabLayout.OnExpandListener {
public final static String KEY_ACTION = "action";
@@ -137,7 +135,6 @@ public final class GalleryListScene extends BaseScene
public final static String KEY_LIST_URL_BUILDER = "list_url_builder";
public final static String KEY_HAS_FIRST_REFRESH = "has_first_refresh";
public final static String KEY_STATE = "state";
private static final int BACK_PRESSED_INTERVAL = 2000;
private final static int STATE_NORMAL = 0;
private final static int STATE_SIMPLE_SEARCH = 1;
private final static int STATE_SEARCH = 2;
@@ -165,8 +162,6 @@ public final class GalleryListScene extends BaseScene
new ActivityResultContracts.PickVisualMedia(),
result -> mSearchLayout.setImageUri(result));
@Nullable
private SearchBar mSearchBar;
@Nullable
private View mSearchFab;
@Nullable
private final Animator.AnimatorListener mSearchFabAnimatorListener = new SimpleAnimatorListener() {
@@ -197,12 +192,6 @@ public final class GalleryListScene extends BaseScene
@Nullable
private GalleryListHelper mHelper;
@Nullable
private DrawerArrowDrawable mLeftDrawable;
@Nullable
private AddDeleteDrawable mRightDrawable;
@Nullable
private SearchBarMover mSearchBarMover;
@Nullable
private AddDeleteDrawable mActionFabDrawable;
@Nullable
private List<QuickSearch> mQuickSearchList;
@@ -278,6 +267,11 @@ public final class GalleryListScene extends BaseScene
}
}
@Override
public int getMenuResId() {
return R.menu.scene_gallery_list_searchbar_menu;
}
public static void startScene(SceneFragment scene, ListUrlBuilder lub) {
scene.startScene(getStartAnnouncer(lub));
}
@@ -328,9 +322,7 @@ public final class GalleryListScene extends BaseScene
mHelper.refresh();
}
setState(STATE_NORMAL);
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
showSearchBar();
}
@Override
@@ -339,9 +331,9 @@ public final class GalleryListScene extends BaseScene
Context context = getContext();
AssertUtils.assertNotNull(context);
mClient = EhApplication.getEhClient(context);
mDownloadManager = EhApplication.getDownloadManager(context);
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context);
mClient = EhApplication.getEhClient();
mDownloadManager = EhApplication.getDownloadManager();
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter();
mDownloadInfoListener = new DownloadManager.DownloadInfoListener() {
@Override
@@ -441,14 +433,14 @@ public final class GalleryListScene extends BaseScene
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener);
}
private void setSearchBarHint(SearchBar searchBar) {
searchBar.setEditTextHint(getString(EhUrl.SITE_EX == Settings.getGallerySite() ?
private void setSearchBarHint() {
setEditTextHint(getString(EhUrl.SITE_EX == Settings.getGallerySite() ?
R.string.gallery_list_search_bar_hint_exhentai :
R.string.gallery_list_search_bar_hint_e_hentai));
}
private void setSearchBarSuggestionProvider(SearchBar searchBar) {
searchBar.setSuggestionProvider(text -> {
private void setSearchBarSuggestionProvider() {
setSuggestionProvider(text -> {
GalleryDetailUrlParser.Result result1 = GalleryDetailUrlParser.parse(text, false);
if (result1 != null) {
return Collections.singletonList(new GalleryDetailUrlSuggestion(result1.gid, result1.token));
@@ -508,12 +500,11 @@ public final class GalleryListScene extends BaseScene
: R.id.search_normal_search);
// Update search edit text
if (!TextUtils.isEmpty(keyword) && null != mSearchBar && !mIsTopList) {
if (!TextUtils.isEmpty(keyword) && !mIsTopList) {
if (builder.getMode() == ListUrlBuilder.MODE_TAG) {
keyword = wrapTagKeyword(keyword);
}
mSearchBar.setText(keyword);
mSearchBar.cursorToEnd();
setSearchBarText(keyword);
}
// Update title
@@ -521,9 +512,7 @@ public final class GalleryListScene extends BaseScene
if (null == title) {
title = resources.getString(R.string.search);
}
if (null != mSearchBar) {
mSearchBar.setTitle(title);
}
setSearchBarHint(title);
// Update nav checked item
int checkedItemId;
@@ -546,8 +535,8 @@ public final class GalleryListScene extends BaseScene
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
public View onCreateViewWithToolbar(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_gallery_list, container, false);
Context context = getContext();
@@ -562,14 +551,15 @@ public final class GalleryListScene extends BaseScene
mRecyclerView = mContentLayout.getRecyclerView();
FastScroller fastScroller = mContentLayout.getFastScroller();
mSearchLayout = (SearchLayout) ViewUtils.$$(mainLayout, R.id.search_layout);
mSearchBar = (SearchBar) ViewUtils.$$(mainLayout, R.id.search_bar);
mFabLayout = (FabLayout) ViewUtils.$$(mainLayout, R.id.fab_layout);
mSearchFab = ViewUtils.$$(mainLayout, R.id.search_fab);
mSearchFab = ViewUtils.$$(container, R.id.search_fab);
ViewCompat.setWindowInsetsAnimationCallback(view, new WindowInsetsAnimationHelper(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,
mFabLayout,
(View) mSearchFab.getParent()
));
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
container.addView(mFabLayout);
int paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar);
int paddingBottomFab = resources.getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab);
@@ -590,14 +580,12 @@ public final class GalleryListScene extends BaseScene
fastScroller.setPadding(fastScroller.getPaddingLeft(), fastScroller.getPaddingTop() + paddingTopSB,
fastScroller.getPaddingRight(), fastScroller.getPaddingBottom());
mLeftDrawable = new DrawerArrowDrawable(context, ResourcesKt.resolveColor(getTheme(), android.R.attr.colorControlNormal));
mRightDrawable = new AddDeleteDrawable(context, ResourcesKt.resolveColor(getTheme(), android.R.attr.colorControlNormal));
mSearchBar.setLeftDrawable(mLeftDrawable);
mSearchBar.setRightDrawable(mRightDrawable);
mSearchBar.setHelper(this);
mSearchBar.setOnStateChangeListener(this);
setSearchBarHint(mSearchBar);
setSearchBarSuggestionProvider(mSearchBar);
setOnApplySearch((query) -> {
onApplySearch(query);
return null;
});
setSearchBarHint();
setSearchBarSuggestionProvider();
mSearchLayout.setHelper(this);
mSearchLayout.setPadding(mSearchLayout.getPaddingLeft(), mSearchLayout.getPaddingTop() + paddingTopSB,
@@ -613,10 +601,12 @@ public final class GalleryListScene extends BaseScene
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable);
mSearchFab.setOnClickListener(this);
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView, mSearchLayout);
mSearchFab.setOnClickListener( (v) -> {
if (STATE_NORMAL != mState) {
onApplySearch();
hideSoftInput();
}
});
// Update list url builder
onUpdateUrlBuilder();
@@ -639,10 +629,6 @@ public final class GalleryListScene extends BaseScene
public void onDestroyView() {
super.onDestroyView();
if (null != mSearchBarMover) {
mSearchBarMover.cancelAnimation();
mSearchBarMover = null;
}
if (null != mHelper) {
mHelper.destroy();
if (1 == mHelper.getShownViewIndex()) {
@@ -654,17 +640,14 @@ public final class GalleryListScene extends BaseScene
mRecyclerView = null;
}
if (null != mFabLayout) {
((ViewGroup)mFabLayout.getParent()).removeView(mFabLayout);
removeAboveSnackView(mFabLayout);
mFabLayout = null;
}
mAdapter = null;
mSearchLayout = null;
mSearchBar = null;
mSearchFab = null;
mViewTransition = null;
mLeftDrawable = null;
mRightDrawable = null;
mActionFabDrawable = null;
}
@@ -793,6 +776,8 @@ public final class GalleryListScene extends BaseScene
if (null != mFabLayout && mFabLayout.isExpanded()) {
return false;
}
if (isSearchViewShown())
return false;
switch (mState) {
case STATE_NORMAL:
return getStackIndex() == 0;
@@ -806,6 +791,10 @@ public final class GalleryListScene extends BaseScene
@Override
public void onBackPressed() {
if (isSearchViewShown()) {
hideSearchView();
return;
}
if (null != mFabLayout && mFabLayout.isExpanded()) {
mFabLayout.setExpanded(false);
return;
@@ -846,14 +835,6 @@ public final class GalleryListScene extends BaseScene
return true;
}
@Override
public void onClick(View v) {
if (STATE_NORMAL != mState && null != mSearchBar) {
mSearchBar.applySearch();
hideSoftInput();
}
}
@Override
public void onClickPrimaryFab(FabLayout view, FloatingActionButton fab) {
if (STATE_NORMAL == mState) {
@@ -964,6 +945,18 @@ public final class GalleryListScene extends BaseScene
}
}
@Override
public void onSearchViewExpanded() {
super.onSearchViewExpanded();
selectSearchFab(true);
}
@Override
public void onSearchViewHidden() {
super.onSearchViewHidden();
if (mState == STATE_NORMAL) selectActionFab(true);
}
public boolean onItemLongClick(int position) {
final Context context = getContext();
final MainActivity activity = getMainActivity();
@@ -1030,7 +1023,7 @@ public final class GalleryListScene extends BaseScene
}
break;
case 3: // Move
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager(context).getLabelList();
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager().getLabelList();
List<String> labelList = new ArrayList<>(labelRawList.size() + 1);
labelList.add(getString(R.string.default_download_label_name));
for (int i = 0, n = labelRawList.size(); i < n; i++) {
@@ -1157,8 +1150,7 @@ public final class GalleryListScene extends BaseScene
}
private void setState(@State int state, boolean animation) {
if (null == mSearchBar || null == mSearchBarMover ||
null == mViewTransition || null == mSearchLayout) {
if (null == mViewTransition || null == mSearchLayout) {
return;
}
@@ -1166,71 +1158,49 @@ public final class GalleryListScene extends BaseScene
int oldState = mState;
mState = state;
showSearchBar();
onStateChange(state);
switch (oldState) {
case STATE_NORMAL:
if (state == STATE_SIMPLE_SEARCH) {
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
selectSearchFab(animation);
} else if (state == STATE_SEARCH) {
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMover.returnSearchBarPosition();
selectSearchFab(animation);
} else if (state == STATE_SEARCH_SHOW_LIST) {
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
selectSearchFab(animation);
}
break;
case STATE_SIMPLE_SEARCH:
if (state == STATE_NORMAL) {
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMover.returnSearchBarPosition();
selectActionFab(animation);
} else if (state == STATE_SEARCH) {
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMover.returnSearchBarPosition();
} else if (state == STATE_SEARCH_SHOW_LIST) {
mViewTransition.showView(1, animation);
mSearchLayout.scrollSearchContainerToTop();
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
}
break;
case STATE_SEARCH:
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMover.returnSearchBarPosition();
selectActionFab(animation);
} else if (state == STATE_SIMPLE_SEARCH) {
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
} else if (state == STATE_SEARCH_SHOW_LIST) {
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
}
break;
case STATE_SEARCH_SHOW_LIST:
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_NORMAL, animation);
mSearchBarMover.returnSearchBarPosition();
selectActionFab(animation);
} else if (state == STATE_SIMPLE_SEARCH) {
mViewTransition.showView(0, animation);
mSearchBar.setState(SearchBar.STATE_SEARCH_LIST, animation);
mSearchBarMover.returnSearchBarPosition();
} else if (state == STATE_SEARCH) {
mSearchBar.setState(SearchBar.STATE_SEARCH, animation);
mSearchBarMover.returnSearchBarPosition();
}
break;
}
@@ -1238,7 +1208,6 @@ public final class GalleryListScene extends BaseScene
((StageActivity) requireActivity()).updateBackPressCallBackStatus();
}
@Override
public void onClickTitle() {
if (mState == STATE_NORMAL) {
setState(STATE_SIMPLE_SEARCH);
@@ -1246,44 +1215,15 @@ public final class GalleryListScene extends BaseScene
}
@Override
public void onClickLeftIcon() {
if (null == mSearchBar) {
return;
}
if (mSearchBar.getState() == SearchBar.STATE_NORMAL) {
toggleDrawer(Gravity.LEFT);
public boolean onMenuItemClick(@NonNull MenuItem item) {
if (mState == STATE_NORMAL) {
setState(STATE_SEARCH);
} else {
setState(STATE_NORMAL);
}
return true;
}
@Override
public void onClickRightIcon() {
if (null == mSearchBar) {
return;
}
if (mSearchBar.getState() == SearchBar.STATE_NORMAL) {
setState(STATE_SEARCH);
} else {
if (mSearchBar.getEditText().length() == 0) {
setState(STATE_NORMAL);
} else {
// Clear
mSearchBar.setText("");
}
}
}
@Override
public void onSearchEditTextClick() {
if (mState == STATE_SEARCH) {
setState(STATE_SEARCH_SHOW_LIST);
}
}
@Override
public void onApplySearch(String query) {
if (null == mUrlBuilder || null == mHelper || null == mSearchLayout) {
return;
@@ -1311,12 +1251,6 @@ public final class GalleryListScene extends BaseScene
setState(STATE_NORMAL);
}
@Override
public void onSearchEditTextBackPressed() {
onBackPressed();
}
@Override
public void onReceiveContent(Uri uri) {
if (mSearchLayout == null || uri == null) {
return;
@@ -1336,38 +1270,10 @@ public final class GalleryListScene extends BaseScene
public void onEndDragHandler() {
// Restore right drawer
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
if (null != mSearchBarMover) {
mSearchBarMover.returnSearchBarPosition();
}
showSearchBar();
}
@Override
public void onStateChange(SearchBar searchBar, int newState, int oldState, boolean animation) {
if (null == mLeftDrawable || null == mRightDrawable) {
return;
}
switch (oldState) {
default:
case SearchBar.STATE_NORMAL:
mLeftDrawable.setArrow(animation ? ANIMATE_TIME : 0);
mRightDrawable.setDelete(animation ? ANIMATE_TIME : 0);
break;
case SearchBar.STATE_SEARCH:
if (newState == SearchBar.STATE_NORMAL) {
mLeftDrawable.setMenu(animation ? ANIMATE_TIME : 0);
mRightDrawable.setAdd(animation ? ANIMATE_TIME : 0);
}
break;
case SearchBar.STATE_SEARCH_LIST:
if (newState == STATE_NORMAL) {
mLeftDrawable.setMenu(animation ? ANIMATE_TIME : 0);
mRightDrawable.setAdd(animation ? ANIMATE_TIME : 0);
}
break;
}
public void onStateChange(int newState) {
if (newState == STATE_NORMAL || newState == STATE_SIMPLE_SEARCH) {
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.RIGHT);
@@ -1379,9 +1285,7 @@ public final class GalleryListScene extends BaseScene
@Override
public void onChangeSearchMode() {
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
showSearchBar();
}
@Override
@@ -1396,32 +1300,9 @@ public final class GalleryListScene extends BaseScene
}
}
// SearchBarMover.Helper
@Override
public boolean isValidView(RecyclerView recyclerView) {
return (mState == STATE_NORMAL && recyclerView == mRecyclerView) ||
(mState == STATE_SEARCH && recyclerView == mSearchLayout);
}
// SearchBarMover.Helper
@Override
public RecyclerView getValidRecyclerView() {
if (mState == STATE_NORMAL || mState == STATE_SIMPLE_SEARCH) {
return mRecyclerView;
} else {
return mSearchLayout;
}
}
// SearchBarMover.Helper
@Override
public boolean forceShowSearchBar() {
return mState == STATE_SIMPLE_SEARCH || mState == STATE_SEARCH_SHOW_LIST;
}
private void onGetGalleryListSuccess(GalleryListParser.Result result, int taskId) {
if (mHelper != null && mSearchBarMover != null &&
mHelper.isCurrentTask(taskId)) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
String emptyString = getString(mUrlBuilder.getMode() == ListUrlBuilder.MODE_SUBSCRIPTION && result.noWatchedTags
? R.string.gallery_list_empty_hit_subscription
: R.string.gallery_list_empty_hit);
@@ -1440,8 +1321,7 @@ public final class GalleryListScene extends BaseScene
}
private void onGetGalleryListFailure(Exception e, int taskId) {
if (mHelper != null && mSearchBarMover != null &&
mHelper.isCurrentTask(taskId)) {
if (mHelper != null && mHelper.isCurrentTask(taskId)) {
mHelper.onGetException(taskId, e);
}
}
@@ -1571,7 +1451,7 @@ public final class GalleryListScene extends BaseScene
mRecyclerView.outOfCustomChoiceMode();
}
DownloadManager downloadManager = EhApplication.getDownloadManager(context);
DownloadManager downloadManager = EhApplication.getDownloadManager();
DownloadInfo downloadInfo = downloadManager.getDownloadInfo(mGi.gid);
if (downloadInfo == null) {
return;
@@ -1651,7 +1531,7 @@ public final class GalleryListScene extends BaseScene
}
}
private abstract class UrlSuggestion extends SearchBar.Suggestion {
private abstract class UrlSuggestion extends Suggestion {
@Override
public CharSequence getText(TextView textView) {
if (textView.getId() == android.R.id.text1) {
@@ -1765,23 +1645,24 @@ public final class GalleryListScene extends BaseScene
return;
}
int prevGid = 0, nextGid = 0;
if (mIsTopList) {
mUrlBuilder.setNextGid(page);
if (jumpTo != null) {
pgCounter = Integer.parseInt(jumpTo);
mUrlBuilder.setNextGid(pgCounter);
nextGid = pgCounter;
} else {
nextGid = page;
}
} else {
int prevGid = 0, nextGid = 0;
if (page != 0) {
if (page >= mHelper.getPageForTop())
nextGid = minGid;
else
prevGid = maxGid;
}
mUrlBuilder.setPrevGid(prevGid);
mUrlBuilder.setNextGid(nextGid);
} else if (jumpTo != null) {
nextGid = minGid;
} else if (page != 0) {
if (page >= mHelper.getPageForTop())
nextGid = minGid;
else
prevGid = maxGid;
}
mUrlBuilder.setPrevGid(prevGid);
mUrlBuilder.setNextGid(nextGid);
mUrlBuilder.setJumpTo(jumpTo);
jumpTo = null;
@@ -1834,9 +1715,7 @@ public final class GalleryListScene extends BaseScene
@Override
public void onShowView(View hiddenView, View shownView) {
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
showSearchBar();
showActionFab();
}
@@ -1846,11 +1725,9 @@ public final class GalleryListScene extends BaseScene
}
@Override
protected void onScrollToPosition(int postion) {
if (0 == postion) {
if (null != mSearchBarMover) {
mSearchBarMover.showSearchBar();
}
protected void onScrollToPosition(int position) {
if (0 == position) {
showSearchBar();
showActionFab();
}
}
@@ -96,7 +96,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
Context context = getContext();
AssertUtils.assertNotNull(context);
mClient = EhApplication.getEhClient(context);
mClient = EhApplication.getEhClient();
if (savedInstanceState == null) {
onInit();
} else {
@@ -148,7 +148,7 @@ public class GalleryPreviewsScene extends ToolbarScene {
mAdapter = new GalleryPreviewAdapter();
mRecyclerView.setAdapter(mAdapter);
int columnWidth = resources.getDimensionPixelOffset(Settings.getThumbSizeResId());
int columnWidth = Settings.getThumbSize();
AutoGridLayoutManager layoutManager = new AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16));
layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE);
mRecyclerView.setLayoutManager(layoutManager);
@@ -72,9 +72,7 @@ class HistoryScene : ToolbarScene() {
}
})
}
private val mDownloadManager: DownloadManager by lazy {
EhApplication.getDownloadManager(requireContext())
}
private val mDownloadManager = EhApplication.downloadManager
private val mDownloadInfoListener: DownloadInfoListener by lazy {
object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
@@ -99,9 +97,7 @@ class HistoryScene : ToolbarScene() {
override fun onUpdateLabels() {}
}
}
private val mFavouriteStatusRouter: FavouriteStatusRouter by lazy {
EhApplication.getFavouriteStatusRouter(requireContext())
}
private val mFavouriteStatusRouter = EhApplication.favouriteStatusRouter
private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener by lazy {
FavouriteStatusRouter.Listener { _: Long, _: Int ->
mAdapter.notifyDataSetChanged()
@@ -302,7 +298,7 @@ class HistoryScene : ToolbarScene() {
}
3 -> {
val labelRawList = EhApplication.getDownloadManager(context).labelList
val labelRawList = EhApplication.downloadManager.labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
labelList.add(getString(R.string.default_download_label_name))
var i = 0
@@ -373,7 +369,7 @@ class HistoryScene : ToolbarScene() {
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
val downloadManager = EhApplication.getDownloadManager(requireContext())
val downloadManager = EhApplication.downloadManager
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
downloadManager.changeLabel(listOf(downloadInfo), label)
@@ -99,7 +99,7 @@ public final class ProgressScene extends BaseScene implements View.OnClickListen
.setArgs(mGid, mPToken, mPage)
.setCallback(new GetGalleryTokenListener(context,
activity.getStageId(), getTag()));
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
return true;
}
return false;
@@ -0,0 +1,311 @@
package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.TextUtils
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView
import com.google.android.material.shape.MaterialShapeDrawable
import com.hippo.app.BaseDialogBuilder
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhTagDatabase
import com.hippo.ehviewer.widget.SearchDatabase
import com.hippo.scene.StageActivity
abstract class SearchBarScene : ToolbarScene() {
private var mSearchView: SearchView? = null
private var mRecyclerView: RecyclerView? = null
private var mSuggestionList: List<Suggestion>? = null
private var mSuggestionAdapter: SuggestionAdapter? = null
private var mSuggestionProvider: SuggestionProvider? = null
private var mAllowEmptySearch = true
private val mSearchDatabase by lazy { SearchDatabase.getInstance(context) }
private var onApplySearch: (String) -> Unit = {}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view =
inflater.inflate(R.layout.scene_searchbar, container, false) as ViewGroup
mToolbar = view.findViewById(R.id.toolbar)
mAppBarLayout = view.findViewById(R.id.appbar)
mAppBarLayout?.statusBarForeground = MaterialShapeDrawable.createWithElevationOverlay(context)
mSearchView = view.findViewById(R.id.searchview)
mRecyclerView = view.findViewById(R.id.search_bar_list)
mSearchView?.editText?.addTextChangedListener {
updateSuggestions()
}
mSearchView?.editText?.setOnEditorActionListener { _, _, _ ->
(mToolbar as SearchBar).text = mSearchView!!.text
mSearchView?.hide()
onApplySearch()
false
}
mSuggestionList = ArrayList()
mSuggestionAdapter = SuggestionAdapter(LayoutInflater.from(context))
mRecyclerView?.adapter = mSuggestionAdapter
val layoutManager = LinearLayoutManager(context)
mRecyclerView?.layoutManager = layoutManager
mSearchView?.addTransitionListener { _, _, newState ->
if (newState == SearchView.TransitionState.SHOWING)
onSearchViewExpanded()
else if (newState == SearchView.TransitionState.HIDING)
onSearchViewHidden()
}
val contentView = onCreateViewWithToolbar(inflater, view, savedInstanceState)
return view.apply { addView(contentView, 0) }
}
@CallSuper
open fun onSearchViewExpanded() {
(requireActivity() as StageActivity).updateBackPressCallBackStatus()
updateSuggestions()
}
@CallSuper
open fun onSearchViewHidden() {
(requireActivity() as StageActivity).updateBackPressCallBackStatus()
}
fun setSearchBarHint(hint: String?) {
(mToolbar as? SearchBar)?.hint = hint
}
fun setSearchBarText(text: String?) {
(mToolbar as? SearchBar)?.text = text
}
fun setEditTextHint(hint: String?) {
mSearchView?.editText?.hint = hint
}
fun setEditTextText(text: String?) {
mSearchView?.setText(text)
}
fun setNavigationDrawable(drawable: Drawable?) {
(mToolbar as? SearchBar)?.navigationIcon = drawable
}
override fun onNavigationClick() {
toggleDrawer(Gravity.START)
}
fun setOnApplySearch(lambda: (String) -> Unit) {
onApplySearch = lambda
}
fun onApplySearch() {
val query = mSearchView?.text.toString().trim()
if (!mAllowEmptySearch && query.isEmpty()) {
return
}
mSearchDatabase.addQuery(query)
onApplySearch(query)
}
fun setAllowEmptySearch(allowEmptySearch: Boolean) {
mAllowEmptySearch = allowEmptySearch
}
fun isSearchViewShown(): Boolean {
return mSearchView?.isShowing ?: false
}
fun hideSearchView() {
mSearchView?.hide()
}
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) + "$\""
}
interface SuggestionProvider {
fun providerSuggestions(text: String): List<Suggestion>?
}
abstract class Suggestion {
abstract fun getText(textView: TextView): CharSequence?
abstract fun onClick()
open fun onLongClick(): Boolean {
return false
}
}
private class SuggestionHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val text1 = itemView.findViewById(android.R.id.text1) as TextView
val text2 = itemView.findViewById(android.R.id.text2) as TextView
}
private inner class SuggestionAdapter constructor(private val mInflater: LayoutInflater) :
RecyclerView.Adapter<SuggestionHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionHolder {
return SuggestionHolder(mInflater.inflate(R.layout.item_simple_list_2, parent, false))
}
override fun onBindViewHolder(holder: SuggestionHolder, position: Int) {
val suggestion = mSuggestionList?.get(position)
val text1 = suggestion?.getText(holder.text1)
val text2 = suggestion?.getText(holder.text2)
holder.text1.text = text1
if (text2 == null) {
holder.text2.visibility = View.GONE
holder.text2.text = ""
} else {
holder.text2.visibility = View.VISIBLE
holder.text2.text = text2
}
holder.itemView.setOnClickListener {
mSuggestionList?.run {
if (position < size) {
this[position].onClick()
}
}
}
holder.itemView.setOnLongClickListener {
mSuggestionList?.run {
if (position < size) {
return@setOnLongClickListener this[position].onLongClick()
}
}
return@setOnLongClickListener false
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemCount(): Int {
return mSuggestionList?.size ?: 0
}
}
inner class TagSuggestion constructor(var mHint: String, var mKeyword: String) : Suggestion() {
override fun getText(textView: TextView): CharSequence {
return if (textView.id == android.R.id.text1) {
mKeyword
} else {
mHint
}
}
override fun onClick() {
val edittext = mSearchView?.editText
edittext?.let {
val text = it.text.toString()
var temp = wrapTagKeyword(mKeyword) + " "
if (text.contains(" ")) {
temp = text.substring(0, text.lastIndexOf(" ")) + " " + temp
}
it.setText(temp)
it.setSelection(text.length)
}
}
}
inner class KeywordSuggestion constructor(private val mKeyword: String) : Suggestion() {
override fun getText(textView: TextView): CharSequence? {
return if (textView.id == android.R.id.text1) {
mKeyword
} else {
null
}
}
override fun onClick() {
mSearchView?.editText?.run {
setText(mKeyword)
setSelection(length())
}
}
override fun onLongClick(): Boolean {
BaseDialogBuilder(requireContext())
.setMessage(requireContext().getString(R.string.delete_search_history, mKeyword))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.delete) { _, _ ->
mSearchDatabase.deleteQuery(mKeyword)
updateSuggestions(false)
}
.show()
return true
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateSuggestions(scrollToTop: Boolean = true) {
val suggestions = arrayListOf<Suggestion>()
val text = mSearchView?.editText?.text?.toString() ?: return
mSuggestionProvider?.run {
providerSuggestions(text)?.let { suggestions.addAll(it) }
}
mSearchDatabase.getSuggestions(text, 128).forEach { suggestions.add(KeywordSuggestion(it)) }
val ehTagDatabase = EhTagDatabase.getInstance(context)
if (!TextUtils.isEmpty(text) && ehTagDatabase != null && !text.endsWith(" ")) {
val s = text.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (s.isNotEmpty()) {
val keyword = s[s.size - 1]
val searchHints = ehTagDatabase.suggest(keyword)
for (searchHint in searchHints) {
suggestions.add(TagSuggestion(searchHint.first, searchHint.second))
}
}
}
mSuggestionList = suggestions
mSuggestionAdapter?.notifyDataSetChanged()
if (scrollToTop) {
mRecyclerView?.scrollToPosition(0)
}
}
fun setSuggestionProvider(suggestionProvider: SuggestionProvider) {
mSuggestionProvider = suggestionProvider
}
fun showSearchBar() {
mAppBarLayout?.setExpanded(true)
}
}
@@ -50,7 +50,7 @@ public class SelectSiteScene extends SolidScene implements View.OnClickListener
View view = inflater.inflate(R.layout.scene_select_site, container, false);
mButtonGroup = (MaterialButtonToggleGroup) ViewUtils.$$(view, R.id.button_group);
if (EhApplication.getEhCookieStore(requireContext()).hasSignedIn()) {
if (EhApplication.getEhCookieStore().hasSignedIn()) {
((MaterialButton) view.findViewById(R.id.site_ex)).setChecked(true);
} else {
((MaterialButton) view.findViewById(R.id.site_e)).setChecked(true);
@@ -45,7 +45,6 @@ import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.WebViewUtil;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.IntIdGenerator;
import com.hippo.yorozuya.ViewUtils;
@@ -132,7 +131,7 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
mSignIn = ViewUtils.$$(loginForm, R.id.sign_in);
mSignInViaWebView = (TextView) ViewUtils.$$(loginForm, R.id.sign_in_via_webview);
mSignInViaCookies = (TextView) ViewUtils.$$(loginForm, R.id.sign_in_via_cookies);
mSkipSigningIn = (TextView) ViewUtils.$$(loginForm, R.id.skip_signing_in);
mSkipSigningIn = (TextView) ViewUtils.$$(loginForm, R.id.tourist_mode);
mSignInViaWebView.setPaintFlags(mSignInViaWebView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
mSignInViaCookies.setPaintFlags(mSignInViaCookies.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
@@ -226,10 +225,6 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
} else if (mSignIn == v) {
signIn();
} else if (mSignInViaWebView == v) {
if (!WebViewUtil.available(getContext())) {
showTip(R.string.error_no_webview, BaseScene.LENGTH_SHORT);
return;
}
startScene(new Announcer(WebViewSignInScene.class).setRequestCode(this, REQUEST_CODE_WEBVIEW));
} else if (mSignInViaCookies == v) {
startScene(new Announcer(CookieSignInScene.class).setRequestCode(this, REQUEST_CODE_COOKIE));
@@ -294,7 +289,7 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
.setMethod(EhClient.METHOD_SIGN_IN)
.setArgs(username, password)
.setCallback(callback);
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
mSigningIn = true;
}
@@ -315,7 +310,7 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
EhRequest request = new EhRequest()
.setMethod(EhClient.METHOD_GET_PROFILE)
.setCallback(callback);
EhApplication.getEhClient(context).execute(request);
EhApplication.getEhClient().execute(request);
}
private void redirectTo() {
@@ -346,7 +341,7 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
return;
}
if (EhApplication.getEhCookieStore(context).hasSignedIn()) {
if (EhApplication.getEhCookieStore().hasSignedIn()) {
getProfile();
} else {
mSigningIn = false;
@@ -118,7 +118,7 @@ public class WebViewSignInScene extends SolidScene {
}
private void addCookie(Context context, String domain, Cookie cookie) {
EhApplication.getEhCookieStore(context).addCookie(EhCookieStore.newCookie(cookie, domain, true, true, true));
EhApplication.getEhCookieStore().addCookie(EhCookieStore.newCookie(cookie, domain, true, true, true));
}
@Override
@@ -16,8 +16,6 @@
package com.hippo.ehviewer.widget
import android.annotation.SuppressLint
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import android.os.Bundle
import android.os.Parcelable
import com.hippo.ehviewer.EhApplication
@@ -25,7 +23,12 @@ import com.hippo.ehviewer.FavouriteStatusRouter
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.widget.ContentLayout.ContentHelper
import com.hippo.yorozuya.IntIdGenerator
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
@JvmField
@@ -48,11 +51,11 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
info.favoriteSlot = slot
}
}
EhApplication.getFavouriteStatusRouter().addListener(listener)
EhApplication.favouriteStatusRouter.addListener(listener)
}
fun destroy() {
EhApplication.getFavouriteStatusRouter().removeListener(listener)
EhApplication.favouriteStatusRouter.removeListener(listener)
}
override fun onAddData(data: GalleryInfo?) {
@@ -64,8 +67,8 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
info?.let {
if (maxGid == -1) maxGid = info.gid.toInt()
if (minGid == -1) minGid = info.gid.toInt()
maxGid = Math.max(maxGid.toLong(), info.gid).toInt()
minGid = Math.min(minGid.toLong(), info.gid).toInt()
maxGid = max(maxGid.toLong(), info.gid).toInt()
minGid = min(minGid.toLong(), info.gid).toInt()
map[info.gid] = info
}
}
@@ -97,7 +100,7 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
val bundle = super.saveInstanceState(superState) as Bundle
// TODO It's a bad design
val router = EhApplication.getFavouriteStatusRouter()
val router = EhApplication.favouriteStatusRouter
val id = router.saveDataMap(map)
bundle.putInt(KEY_DATA_MAP, id)
bundle.putInt(KEY_MIN_GID, minGid)
@@ -109,7 +112,7 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
val bundle = state as Bundle
val id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID)
if (id != IntIdGenerator.INVALID_ID) {
val router = EhApplication.getFavouriteStatusRouter()
val router = EhApplication.favouriteStatusRouter
val map = router.restoreDataMap(id)
if (map != null) {
this.map = map
@@ -121,11 +124,9 @@ abstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {
}
fun goTo(time: Long) {
val calendar = Calendar.getInstance()
calendar.timeInMillis = time
val date = calendar.time
val format = SimpleDateFormat("yyyy-MM-dd", Locale.US)
jumpTo = format.format(date)
val formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd", Locale.US).withZone(ZoneOffset.UTC)
jumpTo = formatter.format(Instant.ofEpochMilli(time))
doGoToPage(Int.MAX_VALUE / 2)
}
@@ -1,681 +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.ehviewer.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import com.hippo.app.BaseDialogBuilder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhTagDatabase;
import com.hippo.view.ViewTransition;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.ViewUtils;
import java.util.ArrayList;
import java.util.List;
public class SearchBar extends MaterialCardView implements View.OnClickListener,
TextView.OnEditorActionListener, TextWatcher,
SearchEditText.SearchEditTextListener {
public static final int STATE_NORMAL = 0;
public static final int STATE_SEARCH = 1;
public static final int STATE_SEARCH_LIST = 2;
private static final String STATE_KEY_SUPER = "super";
private static final String STATE_KEY_STATE = "state";
private static final long ANIMATE_TIME = 300L;
private final Rect mRect = new Rect();
private int mState = STATE_NORMAL;
private int mWidth;
private int mHeight;
private int mBaseHeight;
private float mProgress;
private ImageView mMenuButton;
private TextView mTitleTextView;
private ImageView mActionButton;
private SearchEditText mEditText;
private EasyRecyclerView mListView;
private View mListContainer;
private View mListHeader;
private ViewTransition mViewTransition;
private SearchDatabase mSearchDatabase;
private List<Suggestion> mSuggestionList;
private SuggestionAdapter mSuggestionAdapter;
private Helper mHelper;
private OnStateChangeListener mOnStateChangeListener;
private SuggestionProvider mSuggestionProvider;
private boolean mAllowEmptySearch = true;
private boolean mInAnimation;
public SearchBar(Context context) {
super(context);
init(context);
}
public SearchBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SearchBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mSearchDatabase = SearchDatabase.getInstance(getContext());
LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.widget_search_bar, this);
mMenuButton = (ImageView) ViewUtils.$$(this, R.id.search_menu);
mTitleTextView = (TextView) ViewUtils.$$(this, R.id.search_title);
mActionButton = (ImageView) ViewUtils.$$(this, R.id.search_action);
mEditText = (SearchEditText) ViewUtils.$$(this, R.id.search_edit_text);
mListContainer = ViewUtils.$$(this, R.id.list_container);
mListView = (EasyRecyclerView) ViewUtils.$$(mListContainer, R.id.search_bar_list);
mListHeader = ViewUtils.$$(mListContainer, R.id.list_header);
mViewTransition = new ViewTransition(mTitleTextView, mEditText);
mTitleTextView.setOnClickListener(this);
mMenuButton.setOnClickListener(this);
mActionButton.setOnClickListener(this);
mEditText.setSearchEditTextListener(this);
mEditText.setOnEditorActionListener(this);
mEditText.addTextChangedListener(this);
// Get base height
ViewUtils.measureView(this, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mBaseHeight = getMeasuredHeight();
mSuggestionList = new ArrayList<>();
mSuggestionAdapter = new SuggestionAdapter(LayoutInflater.from(getContext()));
mListView.setAdapter(mSuggestionAdapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
mListView.setLayoutManager(layoutManager);
}
private void addListHeader() {
mListHeader.setVisibility(VISIBLE);
}
private void removeListHeader() {
mListHeader.setVisibility(GONE);
}
private void updateSuggestions() {
updateSuggestions(true);
}
private void updateSuggestions(boolean scrollToTop) {
List<Suggestion> suggestions = new ArrayList<>();
String text = mEditText.getText().toString();
if (mSuggestionProvider != null) {
List<Suggestion> providerSuggestions = mSuggestionProvider.providerSuggestions(text);
if (providerSuggestions != null && !providerSuggestions.isEmpty()) {
suggestions.addAll(providerSuggestions);
}
}
String[] keywords = mSearchDatabase.getSuggestions(text, 128);
for (String keyword : keywords) {
suggestions.add(new KeywordSuggestion(keyword));
}
EhTagDatabase ehTagDatabase = EhTagDatabase.getInstance(getContext());
if (!TextUtils.isEmpty(text) && ehTagDatabase != null && !text.endsWith(" ")) {
String[] s = text.split(" ");
if (s.length > 0) {
String keyword = s[s.length - 1];
ArrayList<Pair<String, String>> searchHints = ehTagDatabase.suggest(keyword);
for (Pair<String, String> searchHint : searchHints) {
suggestions.add(new TagSuggestion(searchHint.first, searchHint.second));
}
}
}
mSuggestionList = suggestions;
if (mSuggestionList.size() == 0) {
removeListHeader();
} else {
addListHeader();
}
mSuggestionAdapter.notifyDataSetChanged();
if (scrollToTop) {
mListView.scrollToPosition(0);
}
}
public void setAllowEmptySearch(boolean allowEmptySearch) {
mAllowEmptySearch = allowEmptySearch;
}
public void setEditTextHint(CharSequence hint) {
mEditText.setHint(hint);
}
public void setHelper(Helper helper) {
mHelper = helper;
}
public void setOnStateChangeListener(OnStateChangeListener listener) {
mOnStateChangeListener = listener;
}
public void setSuggestionProvider(SuggestionProvider suggestionProvider) {
mSuggestionProvider = suggestionProvider;
}
public String getText() {
return mEditText.getText().toString();
}
public void setText(String text) {
mEditText.setText(text);
}
public void cursorToEnd() {
mEditText.setSelection(mEditText.getText().length());
}
public void setTitle(String title) {
mTitleTextView.setText(title);
}
public void setSearch(String search) {
mTitleTextView.setText(search);
mEditText.setText(search);
}
public void setLeftDrawable(Drawable drawable) {
mMenuButton.setImageDrawable(drawable);
}
public void setRightDrawable(Drawable drawable) {
mActionButton.setImageDrawable(drawable);
}
public void applySearch() {
String query = mEditText.getText().toString().trim();
if (!mAllowEmptySearch && TextUtils.isEmpty(query)) {
return;
}
// Put it into db
mSearchDatabase.addQuery(query);
// Callback
mHelper.onApplySearch(query);
}
@Override
public void onClick(View v) {
if (v == mTitleTextView) {
mHelper.onClickTitle();
} else if (v == mMenuButton) {
v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
mHelper.onClickLeftIcon();
} else if (v == mActionButton) {
v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
mHelper.onClickRightIcon();
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (v == mEditText) {
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_NULL) {
applySearch();
return true;
}
}
return false;
}
public int getState() {
return mState;
}
public void setState(int state) {
setState(state, true);
}
public void setState(int state, boolean animation) {
if (mState != state) {
int oldState = mState;
mState = state;
switch (oldState) {
default:
case STATE_NORMAL:
mViewTransition.showView(1, animation);
mEditText.requestFocus();
if (state == STATE_SEARCH_LIST) {
showImeAndSuggestionsList(animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
case STATE_SEARCH:
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
} else if (state == STATE_SEARCH_LIST) {
showImeAndSuggestionsList(animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
case STATE_SEARCH_LIST:
hideImeAndSuggestionsList(animation);
if (state == STATE_NORMAL) {
mViewTransition.showView(0, animation);
}
if (mOnStateChangeListener != null) {
mOnStateChangeListener.onStateChange(this, state, oldState, animation);
}
break;
}
}
}
public void showImeAndSuggestionsList(boolean animation) {
// Show ime
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mEditText, 0);
// update suggestion for show suggestions list
updateSuggestions();
// Show suggestions list
if (animation) {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", 1f);
oa.setDuration(ANIMATE_TIME);
oa.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
oa.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mListContainer.setVisibility(View.VISIBLE);
mInAnimation = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mInAnimation = false;
}
});
oa.setAutoCancel(true);
oa.start();
} else {
mListContainer.setVisibility(View.VISIBLE);
setProgress(1f);
}
}
private void hideImeAndSuggestionsList(boolean animation) {
// Hide ime
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getWindowToken(), 0);
// Hide suggestions list
if (animation) {
ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress", 0f);
oa.setDuration(ANIMATE_TIME);
oa.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR);
oa.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mInAnimation = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mListContainer.setVisibility(View.GONE);
mInAnimation = false;
}
});
oa.setAutoCancel(true);
oa.start();
} else {
setProgress(0f);
mListContainer.setVisibility(View.GONE);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mListContainer.getVisibility() == View.VISIBLE && (!mInAnimation || mHeight == 0)) {
mWidth = right - left;
mHeight = bottom - top;
}
}
public float getProgress() {
return mProgress;
}
@Keep
public void setProgress(float progress) {
mProgress = progress;
invalidate();
}
public SearchEditText getEditText() {
return mEditText;
}
@Override
public void draw(@NonNull Canvas canvas) {
if (mInAnimation && mHeight != 0) {
final int state = canvas.save();
int bottom = MathUtils.lerp(mBaseHeight, mHeight, mProgress);
mRect.set(0, 0, mWidth, bottom);
setClipBounds(mRect);
canvas.clipRect(mRect);
super.draw(canvas);
canvas.restoreToCount(state);
} else {
setClipBounds(null);
super.draw(canvas);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Empty
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Empty
}
@Override
public void afterTextChanged(Editable s) {
updateSuggestions();
}
@Override
public void onClick() {
mHelper.onSearchEditTextClick();
}
@Override
public void onBackPressed() {
mHelper.onSearchEditTextBackPressed();
}
@Override
public void onReceiveContent(Uri uri) {
mHelper.onReceiveContent(uri);
}
@Override
public Parcelable onSaveInstanceState() {
final Bundle state = new Bundle();
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
state.putInt(STATE_KEY_STATE, mState);
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof final Bundle savedState) {
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
setState(savedState.getInt(STATE_KEY_STATE), false);
}
}
private String wrapTagKeyword(String keyword) {
keyword = keyword.trim();
int index1 = keyword.indexOf(':');
if (index1 == -1 || index1 >= keyword.length() - 1) {
// Can't find :, or : is the last char
return keyword;
}
if (keyword.charAt(index1 + 1) == '"') {
// The char after : is ", the word must be quoted
return keyword;
}
int index2 = keyword.indexOf(' ');
if (index2 <= index1) {
// Can't find space, or space is before :
return keyword;
}
return keyword.substring(0, index1 + 1) + "\"" + keyword.substring(index1 + 1) + "$\"";
}
public interface Helper {
void onClickTitle();
void onClickLeftIcon();
void onClickRightIcon();
void onSearchEditTextClick();
void onApplySearch(String query);
void onSearchEditTextBackPressed();
default void onReceiveContent(Uri uri) {
}
}
public interface OnStateChangeListener {
void onStateChange(SearchBar searchBar, int newState, int oldState, boolean animation);
}
public interface SuggestionProvider {
List<Suggestion> providerSuggestions(String text);
}
public abstract static class Suggestion {
public abstract CharSequence getText(TextView textView);
public abstract void onClick();
public boolean onLongClick() {
return false;
}
}
private static class SuggestionHolder extends RecyclerView.ViewHolder {
TextView text1;
TextView text2;
public SuggestionHolder(@NonNull View itemView) {
super(itemView);
text1 = itemView.findViewById(android.R.id.text1);
text2 = itemView.findViewById(android.R.id.text2);
}
}
private class SuggestionAdapter extends RecyclerView.Adapter<SuggestionHolder> {
private final LayoutInflater mInflater;
private SuggestionAdapter(LayoutInflater inflater) {
mInflater = inflater;
}
@NonNull
@Override
public SuggestionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new SuggestionHolder(mInflater.inflate(R.layout.item_simple_list_2, parent, false));
}
@Override
public void onBindViewHolder(@NonNull SuggestionHolder holder, int position) {
Suggestion suggestion = mSuggestionList.get(position);
CharSequence text1 = suggestion.getText(holder.text1);
CharSequence text2 = suggestion.getText(holder.text2);
holder.text1.setText(text1);
if (text2 == null) {
holder.text2.setVisibility(View.GONE);
holder.text2.setText("");
} else {
holder.text2.setVisibility(View.VISIBLE);
holder.text2.setText(text2);
}
holder.itemView.setOnClickListener(v -> {
if (position < mSuggestionList.size()) {
mSuggestionList.get(position).onClick();
}
});
holder.itemView.setOnLongClickListener(v -> {
if (position < mSuggestionList.size()) {
return mSuggestionList.get(position).onLongClick();
} else {
return false;
}
});
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return mSuggestionList.size();
}
}
public class TagSuggestion extends SearchBar.Suggestion {
public String mHint;
public String mKeyword;
private TagSuggestion(String hint, String keyword) {
mHint = hint;
mKeyword = keyword;
}
@Override
public CharSequence getText(TextView textView) {
if (textView.getId() == android.R.id.text1) {
return mKeyword;
} else {
return mHint;
}
}
@Override
public void onClick() {
Editable editable = mEditText.getText();
if (editable != null) {
String text = editable.toString();
String temp = wrapTagKeyword(mKeyword) + " ";
if (text.contains(" ")) {
temp = text.substring(0, text.lastIndexOf(" ")) + " " + temp;
}
mEditText.setText(temp);
mEditText.setSelection(editable.length());
}
}
}
public class KeywordSuggestion extends Suggestion {
private final String mKeyword;
private KeywordSuggestion(String keyword) {
mKeyword = keyword;
}
@Override
public CharSequence getText(TextView textView) {
if (textView.getId() == android.R.id.text1) {
return mKeyword;
} else {
return null;
}
}
@Override
public void onClick() {
mEditText.setText(mKeyword);
mEditText.setSelection(mEditText.length());
}
@Override
public boolean onLongClick() {
new BaseDialogBuilder(getContext())
.setMessage(getContext().getString(R.string.delete_search_history, mKeyword))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.delete, (dialog, which) -> {
mSearchDatabase.deleteQuery(mKeyword);
updateSuggestions(false);
})
.show();
return true;
}
}
}
@@ -1,118 +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.ClipData;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ContentInfo;
import android.view.KeyEvent;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.widget.AppCompatEditText;
import com.hippo.util.ExceptionUtils;
public class SearchEditText extends AppCompatEditText {
private SearchEditTextListener mListener;
public SearchEditText(Context context) {
super(context);
}
public SearchEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSearchEditTextListener(SearchEditTextListener listener) {
mListener = listener;
}
@Override
public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
// special case for the back key, we do not even try to send it
// to the drop down list but instead, consume it immediately
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.handleUpEvent(event);
}
if (event.isTracking() && !event.isCanceled()) {
// TODO stopSelectionActionMode
if (mListener != null) {
mListener.onBackPressed();
return true;
}
}
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP && mListener != null) {
mListener.onClick();
}
try {
return super.onTouchEvent(event);
} catch (Throwable t) {
// Some devices crash here.
// I don't why.
ExceptionUtils.throwIfFatal(t);
return false;
}
}
@RequiresApi(api = Build.VERSION_CODES.S)
@Nullable
@Override
public ContentInfo onReceiveContent(@NonNull ContentInfo payload) {
ClipData clipData = payload.getClip();
if (clipData.getItemCount() == 1) {
if (mListener != null) {
mListener.onReceiveContent(clipData.getItemAt(0).getUri());
}
}
return super.onReceiveContent(payload);
}
public interface SearchEditTextListener {
void onClick();
void onBackPressed();
void onReceiveContent(Uri uri);
}
}
@@ -28,6 +28,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.IntDef
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DefaultItemAnimator
@@ -329,7 +330,12 @@ class SearchLayout @JvmOverloads constructor(
inner class IdentifiedChip @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : Chip(context, attrs, R.attr.filterChipStyle) {
) : Chip(
ContextThemeWrapper(
context,
com.google.android.material.R.style.Widget_Material3_Chip_Filter
), attrs
) {
var idt = 0
}
+40 -10
View File
@@ -23,15 +23,19 @@ import android.graphics.Canvas
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.ALLOCATOR_DEFAULT
import android.graphics.ImageDecoder.ALLOCATOR_SOFTWARE
import android.graphics.ImageDecoder.DecodeException
import android.graphics.ImageDecoder.ImageInfo
import android.graphics.ImageDecoder.Source
import android.graphics.PixelFormat
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toDrawable
import com.hippo.ehviewer.EhApplication
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import kotlin.math.max
class Image private constructor(
source: Source?, drawable: Drawable? = null,
@@ -40,15 +44,27 @@ class Image private constructor(
) {
internal var mObtainedDrawable: Drawable?
private var mBitmap: Bitmap? = null
private var mCanvas: Canvas? = null
init {
mObtainedDrawable = null
source?.let {
mObtainedDrawable = ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, _, _ ->
decoder.allocator = if (hardware) ALLOCATOR_DEFAULT else ALLOCATOR_SOFTWARE
// Sadly we must use software memory since we need copy it to tile buffer, fuck glgallery
// Idk it will cause how much performance regression
} // Should we lazy decode it?
mObtainedDrawable =
ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, info: ImageInfo, src: Source ->
decoder.allocator = if (hardware) ALLOCATOR_DEFAULT else ALLOCATOR_SOFTWARE
// Sadly we must use software memory since we need copy it to tile buffer, fuck glgallery
// Idk it will cause how much performance regression
decoder.setTargetSampleSize(
max(
max(
info.size.width / (2 * screenWidth),
info.size.height / (2 * screenHeight)
), 1
)
)
// Don't
} // Should we lazy decode it?
}
if (mObtainedDrawable == null) {
mObtainedDrawable = drawable!!
@@ -72,6 +88,7 @@ class Image private constructor(
}
mObtainedDrawable?.callback = null
mObtainedDrawable = null
mCanvas = null
mBitmap?.recycle()
mBitmap = null
release()
@@ -80,11 +97,12 @@ class Image private constructor(
private fun prepareBitmap() {
if (mBitmap != null) return
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mCanvas = Canvas(mBitmap!!)
}
private fun updateBitmap() {
prepareBitmap()
mObtainedDrawable!!.draw(Canvas(mBitmap!!))
mObtainedDrawable!!.draw(mCanvas!!)
}
fun render(
@@ -112,12 +130,13 @@ class Image private constructor(
fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) {
check(!hardware) { "Hardware buffer cannot be used in glgallery" }
val bitmap: Bitmap = if (animated) {
val bitmap: Bitmap? = if (animated) {
updateBitmap()
mBitmap!!
mBitmap
} else {
(mObtainedDrawable as BitmapDrawable).bitmap
(mObtainedDrawable as BitmapDrawable?)?.bitmap
}
bitmap ?: return
nativeTexImage(
bitmap,
init,
@@ -137,7 +156,7 @@ class Image private constructor(
val delay: Int
get() {
if (animated)
return 100
return 10
return 0
}
@@ -147,6 +166,16 @@ class Image private constructor(
}
companion object {
var screenWidth: Int = 0
var screenHeight: Int = 0
init {
val context = EhApplication.application
screenWidth = context.resources.displayMetrics.widthPixels
screenHeight = context.resources.displayMetrics.heightPixels
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(stream: FileInputStream, hardware: Boolean = true): Image {
val src = ImageDecoder.createSource(
@@ -158,6 +187,7 @@ class Image private constructor(
return Image(src, hardware = hardware)
}
@Throws(DecodeException::class)
@JvmStatic
fun decode(buffer: ByteBuffer, hardware: Boolean = true, release: () -> Unit? = {}): Image {
val src = ImageDecoder.createSource(buffer)
@@ -1,64 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.image
import android.graphics.drawable.Drawable
import java.io.FileInputStream
class ImageBitmap private constructor(image: Image) {
private var mImage: Image? = image
private var mReferences = 0
@Synchronized
fun obtain(): Boolean {
return if (mImage?.isRecycled == true) {
false
} else {
++mReferences
true
}
}
@Synchronized
fun release() {
--mReferences
if (mReferences <= 0 && !mImage!!.isRecycled) {
mImage!!.recycle()
mImage = null
}
}
fun getDrawable(): Drawable {
check(obtain()) { "Recycled!" }
return mImage!!.mObtainedDrawable as Drawable
}
val width: Int
get() = mImage!!.width
val height: Int
get() = mImage!!.height
companion object {
@JvmStatic
fun decode(stream: FileInputStream): ImageBitmap {
val image = Image.decode(stream)
return ImageBitmap(image)
}
}
}
@@ -15,8 +15,8 @@
*/
package com.hippo.network
import android.content.Context
import android.util.SparseArray
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
class StatusCodeException(val responseCode: Int) : Exception() {
@@ -33,9 +33,8 @@ class StatusCodeException(val responseCode: Int) : Exception() {
private val ERROR_MESSAGE_ARRAY = SparseArray<String>(24)
private const val DEFAULT_ERROR_MESSAGE = "Error response code"
@JvmStatic
fun initialize(context: Context) {
val resources = context.applicationContext.resources
init {
val resources = EhApplication.application.resources
ERROR_MESSAGE_ARRAY.append(400, resources.getString(R.string.error_status_code_400))
ERROR_MESSAGE_ARRAY.append(401, resources.getString(R.string.error_status_code_401))
ERROR_MESSAGE_ARRAY.append(402, resources.getString(R.string.error_status_code_402))
@@ -62,5 +61,4 @@ class StatusCodeException(val responseCode: Int) : Exception() {
ERROR_MESSAGE_ARRAY.append(505, resources.getString(R.string.error_status_code_505))
}
}
}
}
@@ -18,7 +18,6 @@ package com.hippo.scene;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
@@ -154,6 +153,7 @@ public abstract class StageActivity extends EhActivity {
mDelaySceneTagList.addAll(list);
}
mIdGenerator.lazySet(savedInstanceState.getInt(KEY_NEXT_ID));
updateBackPressCallBackStatus();
}
if (mStageId == IntIdGenerator.INVALID_ID) {
@@ -561,12 +561,8 @@ public abstract class StageActivity extends EhActivity {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss();
fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss();
} else {
fragmentManager.beginTransaction().detach(fragment).attach(fragment).commitAllowingStateLoss();
}
fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss();
fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss();
updateBackPressCallBackStatus();
}
@@ -17,16 +17,13 @@ package com.hippo.text
import android.graphics.drawable.Drawable
import android.text.Html.ImageGetter
import com.hippo.conaco.Conaco
import com.hippo.drawable.UnikeryDrawable
import com.hippo.image.ImageBitmap
import com.hippo.widget.ObservedTextView
class URLImageGetter(
private val mTextView: ObservedTextView,
private val mConaco: Conaco<ImageBitmap>
) : ImageGetter {
override fun getDrawable(source: String): Drawable {
return UnikeryDrawable(mTextView, mConaco).apply { load(source) }
return UnikeryDrawable(mTextView).apply { load(source) }
}
}
@@ -1,122 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.util;
import java.util.Comparator;
/**
* Compares each path segment in natural sort order.
* {@code \} and {@code /} are the same. Duplicate {@code \} and {@code /} are ignored.
* Leading {@code \} and {@code /} are ignored, so there is no difference between
* relative path and absolute path.
* {@code /./} and {@code /../} are treated as normal path.
*/
public class PathNaturalComparator implements Comparator<String> {
private static final boolean TYPE_SEPARATOR = false;
private static final boolean TYPE_NORMAL = true;
private final Comparator<String> naturalComparator = new NaturalComparator();
private static int nextSegmentStart(String str, int index) {
if (index >= str.length()) {
return index;
}
boolean type = getType(str.charAt(index));
int i = index + 1;
while (i < str.length() && type == getType(str.charAt(i))) {
i += 1;
}
return i;
}
private static boolean getType(char c) {
if (c == '/' || c == '\\') {
return TYPE_SEPARATOR;
} else {
return TYPE_NORMAL;
}
}
@Override
public int compare(String o1, String o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
int index1 = 0;
int index2 = 0;
while (true) {
String data1;
String data2;
for (; ; ) {
int newIndex1 = nextSegmentStart(o1, index1);
if (newIndex1 == index1) {
data1 = null;
break;
}
if (getType(o1.charAt(newIndex1 - 1)) == TYPE_NORMAL) {
data1 = o1.substring(index1, newIndex1);
index1 = newIndex1;
break;
}
index1 = newIndex1;
}
for (; ; ) {
int newIndex2 = nextSegmentStart(o2, index2);
if (newIndex2 == index2) {
data2 = null;
break;
}
if (getType(o2.charAt(newIndex2 - 1)) == TYPE_NORMAL) {
data2 = o2.substring(index2, newIndex2);
index2 = newIndex2;
break;
}
index2 = newIndex2;
}
if (data1 == null && data2 == null) {
return 0;
}
if (data1 == null) {
return -1;
}
if (data2 == null) {
return 1;
}
int result = naturalComparator.compare(data1, data2);
if (result != 0) {
return result;
}
}
}
}
@@ -16,18 +16,18 @@
package com.hippo.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import com.hippo.ehviewer.R;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
@SuppressLint("SimpleDateFormat")
public final class ReadableTime {
public static final long SECOND_MILLIS = 1000;
@@ -51,38 +51,19 @@ public final class ReadableTime {
R.plurals.minute,
R.plurals.second
};
private static final Calendar sCalendar = Calendar.getInstance();
private static final Object sCalendarLock = new Object();
private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR = new SimpleDateFormat("MMM d");
private static final SimpleDateFormat DATE_FORMAT_WITH_YEAR = new SimpleDateFormat("MMM d, yyyy");
private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR_ZH = new SimpleDateFormat("M月d日");
private static final SimpleDateFormat DATE_FORMAT_WITH_YEAR_ZH = new SimpleDateFormat("yyyy年M月d日");
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yy-MM-dd HH:mm");
private static final Object sDateFormatLock1 = new Object();
private static final SimpleDateFormat FILENAMABLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
private static final Object sDateFormatLock2 = new Object();
private static final DateTimeFormatter DATE_FORMAT_WITHOUT_YEAR = DateTimeFormatter.ofPattern("MMM d");
private static final DateTimeFormatter DATE_FORMAT_WITH_YEAR = DateTimeFormatter.ofPattern("MMM d, yyyy");
private static final DateTimeFormatter DATE_FORMAT_WITHOUT_YEAR_ZH = DateTimeFormatter.ofPattern("M月d日");
private static final DateTimeFormatter DATE_FORMAT_WITH_YEAR_ZH = DateTimeFormatter.ofPattern("yyyy年M月d日");
private static final DateTimeFormatter FILENAMABLE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-SSS");
private static final Object sDateFormatLock = new Object();
private static Resources sResources;
public static void initialize(Context context) {
sResources = context.getApplicationContext().getResources();
}
/*
public static String getDisplayTime(long time) {
if (Settings.getPrettyTime()) {
return getTimeAgo(time);
} else {
return getPlainTime(time);
}
}
*/
public static String getPlainTime(long time) {
synchronized (sDateFormatLock1) {
return DATE_FORMAT.format(new Date(time));
}
}
public static String getTimeAgo(long time) {
Resources resources = sResources;
@@ -111,49 +92,23 @@ public final class ReadableTime {
return resources.getString(R.string.some_days_ago, days);
} else {
synchronized (sCalendarLock) {
Date nowDate = new Date(now);
Date timeDate = new Date(time);
sCalendar.setTime(nowDate);
int nowYear = sCalendar.get(Calendar.YEAR);
sCalendar.setTime(timeDate);
int timeYear = sCalendar.get(Calendar.YEAR);
OffsetDateTime nowDate = Instant.ofEpochMilli(now).atOffset(ZoneOffset.UTC);
OffsetDateTime timeDate = Instant.ofEpochMilli(time).atOffset(ZoneOffset.UTC);
int nowYear = nowDate.getYear();
int timeYear = timeDate.getYear();
boolean isZh = Locale.getDefault().getLanguage().equals("zh");
if (nowYear == timeYear) {
return (isZh ? DATE_FORMAT_WITHOUT_YEAR_ZH : DATE_FORMAT_WITHOUT_YEAR).format(timeDate);
return (isZh ? DATE_FORMAT_WITHOUT_YEAR_ZH : DATE_FORMAT_WITHOUT_YEAR)
.format(timeDate.toInstant().atOffset(ZoneOffset.UTC));
} else {
return (isZh ? DATE_FORMAT_WITH_YEAR_ZH : DATE_FORMAT_WITH_YEAR).format(timeDate);
return (isZh ? DATE_FORMAT_WITH_YEAR_ZH : DATE_FORMAT_WITH_YEAR)
.format(timeDate.toInstant().atOffset(ZoneOffset.UTC));
}
}
}
}
public static String getTimeInterval(long time) {
StringBuilder sb = new StringBuilder();
Resources resources = sResources;
long leftover = time;
boolean start = false;
for (int i = 0; i < SIZE; i++) {
long multiple = MULTIPLES[i];
long quotient = leftover / multiple;
long remainder = leftover % multiple;
if (start || quotient != 0 || i == SIZE - 1) {
if (start) {
sb.append(" ");
}
sb.append(quotient)
.append(" ")
.append(resources.getQuantityString(UNITS[i], (int) quotient));
start = true;
}
leftover = remainder;
}
return sb.toString();
}
public static String getShortTimeInterval(long time) {
StringBuilder sb = new StringBuilder();
Resources resources = sResources;
@@ -173,8 +128,8 @@ public final class ReadableTime {
}
public static String getFilenamableTime(long time) {
synchronized (sDateFormatLock2) {
return FILENAMABLE_DATE_FORMAT.format(new Date(time));
synchronized (sDateFormatLock) {
return FILENAMABLE_DATE_FORMAT.format(Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()));
}
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.util.Log;
import androidx.webkit.WebViewCompat;
public class WebViewUtil {
private static final String TAG = "WebViewUtil";
public static boolean available(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context);
if (webViewPackageInfo == null) {
Log.d(TAG, "WebView not available");
return false;
}
Log.d(TAG, "WebView version: " + webViewPackageInfo.versionName);
return true;
}
}
@@ -107,6 +107,17 @@ public class BatteryView extends AppCompatTextView {
registerReceiver();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAttached) {
unregisterReceiver();
stopCharger();
mAttached = false;
}
} private final Runnable mCharger = new Runnable() {
private int level = 0;
@@ -122,17 +133,6 @@ public class BatteryView extends AppCompatTextView {
}
};
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAttached) {
unregisterReceiver();
stopCharger();
mAttached = false;
}
}
private void registerReceiver() {
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
@@ -1,371 +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.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.hippo.conaco.Conaco;
import com.hippo.conaco.ConacoTask;
import com.hippo.conaco.Unikery;
import com.hippo.drawable.PreciselyClipDrawable;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.image.ImageBitmap;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class LoadImageView extends FixedAspectImageView implements Unikery<ImageBitmap>,
View.OnClickListener, View.OnLongClickListener, Animatable {
public static final int RETRY_TYPE_NONE = 0;
public static final int RETRY_TYPE_CLICK = 1;
public static final int RETRY_TYPE_LONG_CLICK = 2;
private static final String TAG = LoadImageView.class.getSimpleName();
private int mTaskId = Unikery.INVALID_ID;
private Conaco<ImageBitmap> mConaco;
private String mKey;
private String mUrl;
private boolean mUseNetwork;
private int mOffsetX = Integer.MIN_VALUE;
private int mOffsetY = Integer.MIN_VALUE;
private int mClipWidth = Integer.MIN_VALUE;
private int mClipHeight = Integer.MIN_VALUE;
private int mRetryType;
private boolean mFailed;
private boolean mLoadFromDrawable = false;
private ImageBitmap imageBitmap;
public LoadImageView(Context context) {
super(context);
init(context, null, 0);
}
public LoadImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public LoadImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0);
setRetryType(a.getInt(R.styleable.LoadImageView_retryType, 0));
a.recycle();
if (!isInEditMode()) {
mConaco = EhApplication.getConaco(context);
}
}
@Override
public int getTaskId() {
return mTaskId;
}
@Override
public void setTaskId(int id) {
mTaskId = id;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!mLoadFromDrawable) {
if (mFailed) {
onFailure();
} else if (mConaco.isLoading(this)) {
load(mKey, mUrl, mUseNetwork);
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (!mLoadFromDrawable) {
if (mConaco.isLoading(this)) {
try {
// Cancel
mConaco.cancel(this);
} catch (Exception e) {
// Ignore
}
// Clear drawable
clearDrawable();
}
}
}
private Drawable getImageDrawable() {
Drawable drawable = getDrawable();
if (drawable instanceof TransitionDrawable transitionDrawable) {
if (transitionDrawable.getNumberOfLayers() == 2) {
drawable = transitionDrawable.getDrawable(1);
}
}
if (drawable instanceof PreciselyClipDrawable) {
drawable = ((PreciselyClipDrawable) drawable).getWrappedDrawable();
}
return drawable;
}
private void clearDrawable() {
// Set drawable null
setImageDrawable(null);
// Recycle ImageDrawable
if (imageBitmap != null) {
imageBitmap.release();
imageBitmap = null;
}
}
private void clearRetry() {
if (mRetryType == RETRY_TYPE_CLICK) {
setOnClickListener(null);
setClickable(false);
} else if (mRetryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(null);
setLongClickable(false);
}
}
public void setRetryType(@RetryType int retryType) {
if (mRetryType != retryType) {
int oldRetryType = mRetryType;
mRetryType = retryType;
if (mFailed) {
if (oldRetryType == RETRY_TYPE_CLICK) {
setOnClickListener(null);
setClickable(false);
} else if (oldRetryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(null);
setLongClickable(false);
}
if (retryType == RETRY_TYPE_CLICK) {
setOnClickListener(this);
} else if (retryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(this);
}
}
}
}
public void setClip(int offsetX, int offsetY, int clipWidth, int clipHeight) {
mOffsetX = offsetX;
mOffsetY = offsetY;
mClipWidth = clipWidth;
mClipHeight = clipHeight;
}
public void resetClip() {
mOffsetX = Integer.MIN_VALUE;
mOffsetY = Integer.MIN_VALUE;
mClipWidth = Integer.MIN_VALUE;
mClipHeight = Integer.MIN_VALUE;
}
public void load(String key, String url) {
load(key, url, true);
}
public void load(String key, String url, boolean useNetwork) {
if (url == null || (key == null)) {
return;
}
mFailed = false;
clearRetry();
mKey = key;
mUrl = url;
mUseNetwork = useNetwork;
ConacoTask.Builder<ImageBitmap> builder = new ConacoTask.Builder<ImageBitmap>()
.setUnikery(this)
.setKey(key)
.setUrl(url)
.setUseNetwork(useNetwork);
mConaco.load(builder);
}
public void load(Drawable drawable) {
unload();
onPreSetImageDrawable(drawable, true);
setImageDrawable(drawable);
}
public void load(@DrawableRes int id) {
unload();
mLoadFromDrawable = true;
onPreSetImageResource(id, true);
setImageResource(id);
}
public void unload() {
mConaco.cancel(this);
mKey = null;
mUrl = null;
clearDrawable();
}
@Override
public void onMiss(int source) {
if (source == Conaco.SOURCE_MEMORY) {
clearDrawable();
}
}
@Override
public void onRequest() {
}
@Override
public void onProgress(long singleReceivedSize, long receivedSize, long totalSize) {
}
@Override
public void onWait() {
clearDrawable();
}
@Override
public boolean onGetValue(@NonNull ImageBitmap value, int source) {
Drawable drawable;
try {
drawable = value.getDrawable();
} catch (Exception e) {
// The image might be recycled because it is removed from memory cache.
Log.d(TAG, "The image is recycled", e);
return false;
}
clearDrawable();
if (Integer.MIN_VALUE != mOffsetX) {
drawable = new PreciselyClipDrawable(drawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight);
}
onPreSetImageDrawable(drawable, true);
if ((source == Conaco.SOURCE_DISK || source == Conaco.SOURCE_NETWORK) && isShown()) {
Drawable[] layers = new Drawable[2];
layers[0] = new ColorDrawable(Color.TRANSPARENT);
layers[1] = drawable;
TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(300);
} else {
setImageDrawable(drawable);
}
imageBitmap = value;
return true;
}
@Override
public void onFailure() {
mFailed = true;
clearDrawable();
Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.image_failed);
onPreSetImageDrawable(drawable, true);
setImageDrawable(drawable);
if (mRetryType == RETRY_TYPE_CLICK) {
setOnClickListener(this);
} else if (mRetryType == RETRY_TYPE_LONG_CLICK) {
setOnLongClickListener(this);
} else {
// Can't retry, so release
mKey = null;
mUrl = null;
}
}
@Override
public void onCancel() {
mFailed = false;
}
@Override
public void start() {
Drawable drawable = getImageDrawable();
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable) {
animatedImageDrawable.start();
}
}
@Override
public void stop() {
Drawable drawable = getImageDrawable();
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable) {
animatedImageDrawable.stop();
}
}
@Override
public boolean isRunning() {
Drawable drawable = getImageDrawable();
if (drawable instanceof AnimatedImageDrawable animatedImageDrawable) {
return animatedImageDrawable.isRunning();
} else {
return false;
}
}
@Override
public void onClick(@NonNull View v) {
load(mKey, mUrl, true);
}
@Override
public boolean onLongClick(@NonNull View v) {
load(mKey, mUrl, true);
return true;
}
public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) {
}
public void onPreSetImageResource(int resId, boolean isTarget) {
}
@IntDef({RETRY_TYPE_NONE, RETRY_TYPE_CLICK, RETRY_TYPE_LONG_CLICK})
@Retention(RetentionPolicy.SOURCE)
private @interface RetryType {
}
}
@@ -0,0 +1,222 @@
/*
* 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.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 com.hippo.drawable.PreciselyClipDrawable
import com.hippo.ehviewer.R
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
}
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)
}
}
}
}
fun setClip(offsetX: Int, offsetY: Int, clipWidth: Int, clipHeight: Int) {
mOffsetX = offsetX
mOffsetY = offsetY
mClipWidth = clipWidth
mClipHeight = clipHeight
}
fun resetClip() {
mOffsetX = Integer.MIN_VALUE
mOffsetY = Integer.MIN_VALUE
mClipWidth = Integer.MIN_VALUE
mClipHeight = Integer.MIN_VALUE
}
@JvmOverloads
fun load(key: String, url: String, isLoaded: Boolean = false) {
if ((mRequest?.data as? String?) == url)
return
mFailed = false
clearRetry()
task?.dispose()
mRequest =
ImageRequest.Builder(context).data(url).memoryCacheKey(key).diskCacheKey(key).target(
{ onWait() },
{ onFailure() },
{ onGetValue(it, isLoaded) }
).build().also { load(it) }
}
fun load(request: ImageRequest) {
task = context.imageLoader.enqueue(request)
}
private fun reload() {
mRequest?.let { load(it) }
}
private fun onWait() {
mFailed = false
clearDrawable()
}
private fun onGetValue(drawable: Drawable, isLoaded: Boolean) {
var drawable = drawable
clearDrawable()
if (Integer.MIN_VALUE != mOffsetX) {
drawable = PreciselyClipDrawable(drawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)
}
onPreSetImageDrawable(drawable, true)
if (isShown && !isLoaded) {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = ColorDrawable(Color.TRANSPARENT)
layers[1] = drawable
val transitionDrawable = TransitionDrawable(layers)
setImageDrawable(transitionDrawable)
transitionDrawable.startTransition(300)
} else {
setImageDrawable(drawable)
}
}
fun onFailure() {
mFailed = true
clearDrawable()
val drawable = ContextCompat.getDrawable(context, R.drawable.image_failed)
onPreSetImageDrawable(drawable, true)
setImageDrawable(drawable)
when (mRetryType) {
RETRY_TYPE_CLICK -> {
setOnClickListener(this)
}
RETRY_TYPE_LONG_CLICK -> {
setOnLongClickListener(this)
}
else -> {
mRequest = null
}
}
}
override fun onClick(v: View) {
reload()
}
override fun onLongClick(v: View): Boolean {
reload()
return true
}
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
companion object {
const val RETRY_TYPE_NONE = 0
const val RETRY_TYPE_CLICK = 1
const val RETRY_TYPE_LONG_CLICK = 2
}
}
@@ -1,221 +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.widget;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.ViewUtils;
public class SearchBarMover extends RecyclerView.OnScrollListener {
private static final long ANIMATE_TIME = 300L;
private final Helper mHelper;
private final View mSearchBar;
private boolean mShow;
private ValueAnimator mSearchBarMoveAnimator;
public SearchBarMover(Helper helper, View searchBar, RecyclerView... recyclerViews) {
mHelper = helper;
mSearchBar = searchBar;
for (RecyclerView recyclerView : recyclerViews) {
recyclerView.addOnScrollListener(this);
}
}
public void cancelAnimation() {
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE && mHelper.isValidView(recyclerView)) {
returnSearchBarPosition();
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (mHelper.isValidView(recyclerView)) {
int oldBottom = (int) ViewUtils.getY2(mSearchBar);
int offsetYStep = MathUtils.clamp(-dy, -oldBottom, -(int) mSearchBar.getTranslationY());
if (offsetYStep != 0) {
ViewUtils.translationYBy(mSearchBar, offsetYStep);
}
}
}
public void returnSearchBarPosition() {
returnSearchBarPosition(true);
}
@SuppressWarnings("SimplifiableIfStatement")
public void returnSearchBarPosition(boolean animation) {
if (mSearchBar.getHeight() == 0) {
// Layout not called
return;
}
boolean show;
if (mHelper.forceShowSearchBar()) {
show = true;
} else {
RecyclerView recyclerView = mHelper.getValidRecyclerView();
if (recyclerView == null) {
return;
}
if (!recyclerView.isShown()) {
show = true;
} else if (recyclerView.computeVerticalScrollOffset() < mSearchBar.getBottom()) {
show = true;
} else {
show = (int) ViewUtils.getY2(mSearchBar) > (mSearchBar.getHeight()) / 2;
}
}
int offset;
if (show) {
offset = -(int) mSearchBar.getTranslationY();
} else {
offset = -(int) ViewUtils.getY2(mSearchBar);
}
if (offset == 0) {
// No need to scroll
return;
}
if (animation) {
if (mSearchBarMoveAnimator != null) {
if (mShow == show) {
// The same target, no need to do animation
return;
} else {
// Cancel it
mSearchBarMoveAnimator.cancel();
mSearchBarMoveAnimator = null;
}
}
mShow = show;
final ValueAnimator va = ValueAnimator.ofInt(0, offset);
va.setDuration(ANIMATE_TIME);
va.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mSearchBarMoveAnimator = null;
}
});
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (Integer) animation.getAnimatedValue();
int offsetStep = value - lastValue;
lastValue = value;
ViewUtils.translationYBy(mSearchBar, offsetStep);
}
});
mSearchBarMoveAnimator = va;
va.start();
} else {
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
ViewUtils.translationYBy(mSearchBar, offset);
}
}
public void showSearchBar() {
showSearchBar(true);
}
public void showSearchBar(boolean animation) {
if (mSearchBar.getHeight() == 0) {
// Layout not called
return;
}
final int offset = -(int) mSearchBar.getTranslationY();
if (offset == 0) {
// No need to scroll
return;
}
if (animation) {
if (mSearchBarMoveAnimator != null) {
if (mShow) {
// The same target, no need to do animation
return;
} else {
// Cancel it
mSearchBarMoveAnimator.cancel();
mSearchBarMoveAnimator = null;
}
}
mShow = true;
final ValueAnimator va = ValueAnimator.ofInt(0, offset);
va.setDuration(ANIMATE_TIME);
va.addListener(new SimpleAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
mSearchBarMoveAnimator = null;
}
});
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
int lastValue;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (Integer) animation.getAnimatedValue();
int offsetStep = value - lastValue;
lastValue = value;
ViewUtils.translationYBy(mSearchBar, offsetStep);
}
});
mSearchBarMoveAnimator = va;
va.start();
} else {
if (mSearchBarMoveAnimator != null) {
mSearchBarMoveAnimator.cancel();
}
ViewUtils.translationYBy(mSearchBar, offset);
}
}
public interface Helper {
boolean isValidView(RecyclerView recyclerView);
@Nullable
RecyclerView getValidRecyclerView();
boolean forceShowSearchBar();
}
}
@@ -112,6 +112,13 @@ public class TextClock extends AppCompatTextView {
public CharSequence getFormat24Hour() {
return mFormat24;
}
public void setFormat24Hour(CharSequence format) {
mFormat24 = format;
chooseFormat();
onTimeChanged();
} private final Runnable mTicker = new Runnable() {
@Override
public void run() {
@@ -124,13 +131,6 @@ public class TextClock extends AppCompatTextView {
}
};
public void setFormat24Hour(CharSequence format) {
mFormat24 = format;
chooseFormat();
onTimeChanged();
}
public boolean is24HourModeEnabled() {
return DateFormat.is24HourFormat(getContext());
}
@@ -227,6 +227,15 @@ public class TextClock extends AppCompatTextView {
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());
}
private void registerObserver() {
final ContentResolver resolver = getContext().getContentResolver();
resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
}
private void unregisterReceiver() {
getContext().unregisterReceiver(mIntentReceiver);
} private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
@@ -241,15 +250,6 @@ public class TextClock extends AppCompatTextView {
}
};
private void registerObserver() {
final ContentResolver resolver = getContext().getContentResolver();
resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
}
private void unregisterReceiver() {
getContext().unregisterReceiver(mIntentReceiver);
}
private void unregisterObserver() {
final ContentResolver resolver = getContext().getContentResolver();
resolver.unregisterContentObserver(mFormatChangeObserver);
@@ -1,51 +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.yorozuya;
import java.util.concurrent.atomic.AtomicLong;
public final class LongIdGenerator {
public static final long INVALID_ID = -1L;
private final AtomicLong mId = new AtomicLong();
public LongIdGenerator() {
}
public LongIdGenerator(long init) {
setNextId(init);
}
@SuppressWarnings("StatementWithEmptyBody")
public long nextId() {
long id;
while ((id = mId.getAndIncrement()) == INVALID_ID) ;
return id;
}
public void setNextId(long id) {
checkInValidId(id);
mId.set(id);
}
private void checkInValidId(long id) {
if (INVALID_ID == id) {
throw new IllegalStateException("Can't set INVALID_ID");
}
}
}
@@ -1,94 +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;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public final class Messenger {
private static Messenger sInstance;
private final IntIdGenerator mIdGenerator;
private final SparseArray<List<Receiver>> mReceiverListMap;
private Messenger() {
mIdGenerator = new IntIdGenerator();
mReceiverListMap = new SparseArray<>();
}
public static Messenger getInstance() {
if (sInstance == null) {
sInstance = new Messenger();
}
return sInstance;
}
public int newId() {
return mIdGenerator.nextId();
}
private void notifyInternal(int id, Object obj) {
List<Receiver> receiverList = mReceiverListMap.get(id);
if (receiverList != null) {
List<Receiver> temp = new LinkedList<>(receiverList);
for (Receiver r : temp) {
r.onReceive(id, obj);
}
}
}
public void notify(final int id, final Object obj) {
// Make sure do it in UI thread
SimpleHandler.getInstance().post(new Runnable() {
@Override
public void run() {
notifyInternal(id, obj);
}
});
}
public void notifyAtOnce(int id, Object obj) {
notifyInternal(id, obj);
}
public void register(int id, Receiver receiver) {
List<Receiver> receiverList = mReceiverListMap.get(id);
if (receiverList == null) {
receiverList = new ArrayList<>();
mReceiverListMap.put(id, receiverList);
}
receiverList.add(receiver);
}
public void unregister(int id, Receiver receiver) {
List<Receiver> receiverList = mReceiverListMap.get(id);
if (receiverList != null) {
receiverList.remove(receiver);
if (receiverList.isEmpty()) {
mReceiverListMap.remove(id);
}
}
}
public interface Receiver {
void onReceive(final int id, final Object obj);
}
}
@@ -1,34 +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.yorozuya;
import android.view.animation.Animation;
public class SimpleAnimationListener implements Animation.AnimationListener {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}
@@ -1,185 +0,0 @@
/*
* Copyright (C) 2014-2015 Markus Junginger, greenrobot (http://greenrobot.de)
*
* 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.collect;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* Combines a Map with List values to provide simple way to store multiple values for a key (multimap).
* <p/>
* Threading note: All methods are synchronized
*/
public abstract class AbstractMultimap<K, V, C extends Collection<V>> implements Map<K, C> {
protected final boolean threadSafeCollections;
protected Map<K, C> map;
protected AbstractMultimap(Map<K, C> map, boolean threadSafeCollections) {
this.map = map;
this.threadSafeCollections = threadSafeCollections;
}
abstract protected C createNewCollection();
@Override
public void putAll(Map<? extends K, ? extends C> m) {
map.putAll(m);
}
@Override
public synchronized int size() {
return map.size();
}
@Override
public synchronized boolean isEmpty() {
return map.isEmpty();
}
@Override
public synchronized boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public synchronized boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public synchronized C get(Object key) {
return map.get(key);
}
@Override
public synchronized C remove(Object key) {
return map.remove(key);
}
@Override
public synchronized void clear() {
map.clear();
}
@Override
public synchronized Set<K> keySet() {
return map.keySet();
}
@Override
public synchronized Collection<C> values() {
return map.values();
}
@Override
public synchronized boolean equals(Object o) {
return map.equals(o);
}
@Override
public synchronized int hashCode() {
return map.hashCode();
}
/**
* @return number of elements stored for given key after storing the given value.
*/
public synchronized int putElement(K key, V value) {
C collection = map.get(key);
if (collection == null) {
collection = createNewCollection();
map.put(key, collection);
}
collection.add(value);
return collection.size();
}
@Override
public synchronized C put(K key, C value) {
return map.put(key, value);
}
@Override
public synchronized Set<Entry<K, C>> entrySet() {
return map.entrySet();
}
/**
* @return true if the collection was changed.
*/
public synchronized boolean putElements(K key, Collection<V> values) {
C collection = map.get(key);
if (collection == null) {
collection = createNewCollection();
map.put(key, collection);
}
return collection.addAll(values);
}
/**
* @return true if the given element was removed.
*/
public synchronized boolean removeElement(K key, V value) {
C collection = map.get(key);
if (collection == null) {
return false;
} else {
boolean removed = collection.remove(value);
if (collection.isEmpty()) {
map.remove(key);
}
return removed;
}
}
public synchronized int countElements(K key) {
C collection = map.get(key);
if (collection == null) {
return 0;
} else {
return collection.size();
}
}
public synchronized boolean containsElement(K key, V value) {
C collection = map.get(key);
if (collection == null) {
return false;
} else {
return collection.contains(value);
}
}
public synchronized boolean containsElement(V value) {
for (C collection : map.values()) {
if (collection.contains(value)) {
return true;
}
}
return false;
}
public synchronized C valuesElements() {
C all = createNewCollection();
for (C collection : map.values()) {
all.addAll(collection);
}
return all;
}
}
@@ -1,47 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.yorozuya.collect;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Combines a Map with List values to provide simple way to store multiple values for a key.
*/
// Top level class to get rid of 3rd generic collection parameter for more convenient usage.
public class Multimap<K, V> extends AbstractMultimap<K, V, List<V>> {
protected Multimap(Map<K, List<V>> map, boolean threadSafeCollections) {
super(map, threadSafeCollections);
}
public static <K, V> Multimap<K, V> create() {
return new Multimap<>(new HashMap<K, List<V>>(), false);
}
public static <K, V> Multimap<K, V> createWithThreadSafeLists() {
return new Multimap<>(new HashMap<K, List<V>>(), true);
}
protected List<V> createNewCollection() {
return threadSafeCollections ? new CopyOnWriteArrayList<V>() : new ArrayList<V>();
}
}
@@ -1,49 +0,0 @@
/*
* Copyright 2022 Tarsin Norbin
*
* This file is part of EhViewer
*
* EhViewer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* EhViewer is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with EhViewer.
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.yorozuya.collect;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Combines a Map with Set values to provide simple way to store multiple values for a key.
* Like {@link com.hippo.yorozuya.collect.Multimap}, but element values are stored in Sets.
*/
// Top level class to get rid of 3rd generic collection parameter for more convenient usage.
public class MultimapSet<K, V> extends AbstractMultimap<K, V, Set<V>> {
protected MultimapSet(Map<K, Set<V>> map, boolean threadSafeSets) {
super(map, threadSafeSets);
}
public static <K, V> MultimapSet<K, V> create() {
return new MultimapSet<>(new HashMap<K, Set<V>>(), false);
}
public static <K, V> MultimapSet<K, V> createWithThreadSafeLists() {
return new MultimapSet<>(new HashMap<K, Set<V>>(), true);
}
protected Set<V> createNewCollection() {
return threadSafeCollections ? new CopyOnWriteArraySet<V>() : new HashSet<V>();
}
}
@@ -1,408 +0,0 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.yorozuya.collect;
/**
* SparseILArrays map integers to Objects. Unlike a normal array of Objects,
* there can be gaps in the indices. It is intended to be more memory efficient
* than using a HashMap to map Integers to Objects, both because it avoids
* auto-boxing keys and its data structure doesn't rely on an extra entry object
* for each mapping.
*
* <p>Note that this container keeps its mappings in an array data structure,
* using a binary search to find keys. The implementation is not intended to be appropriate for
* data structures
* that may contain large numbers of items. It is generally slower than a traditional
* HashMap, since lookups require a binary search and adds and removes require inserting
* and deleting entries in the array. For containers holding up to hundreds of items,
* the performance difference is not significant, less than 50%.</p>
*
* <p>To help with performance, the container includes an optimization when removing
* keys: instead of compacting its array immediately, it leaves the removed entry marked
* as deleted. The entry can then be re-used for the same key, or compacted later in
* a single garbage collection step of all removed entries. This garbage collection will
* need to be performed at any time the array needs to be grown or the the map size or
* entry values are retrieved.</p>
*
* <p>It is possible to iterate over the items in this container using
* {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
* <code>keyAt(int)</code> with ascending values of the index will return the
* keys in ascending order, or the values corresponding to the keys in ascending
* order in the case of <code>valueAt(int)</code>.</p>
*/
public class SparseILArray<E> implements Cloneable {
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
/**
* Creates a new SparseILArray containing no mappings.
*/
public SparseILArray() {
this(10);
}
/**
* Creates a new SparseILArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public SparseILArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = CollectionUtils.EMPTY_INT_ARRAY;
mValues = CollectionUtils.EMPTY_OBJECT_ARRAY;
} else {
mKeys = new int[initialCapacity];
mValues = new Object[initialCapacity];
}
mSize = 0;
}
@Override
@SuppressWarnings("unchecked")
public SparseILArray<E> clone() {
SparseILArray<E> clone = null;
try {
clone = (SparseILArray<E>) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException e) {
/* ignore */
}
return clone;
}
/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}
/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
/**
* Removes the mapping from the specified key, if there was any.
*/
@SuppressWarnings("unchecked")
public E delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
E deleted = (E) mValues[i];
if (deleted != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
return deleted;
}
}
return null;
}
/**
* Removes the mapping from the specified key, if there was any, returning the old value.
*/
@SuppressWarnings("unchecked")
public E removeReturnOld(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
final E old = (E) mValues[i];
mValues[i] = DELETED;
mGarbage = true;
return old;
}
}
return null;
}
/**
* Alias for {@link #delete(int)}.
*/
public E remove(int key) {
return delete(key);
}
/**
* Removes the mapping at the specified index.
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
/**
* Remove a range of mappings as a batch.
*
* @param index Index to begin at
* @param size Number of mappings to remove
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
private void gc() {
// Log.e("SparseILArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseILArray", "gc end with " + mSize);
}
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
mKeys = CollectionUtils.insert(mKeys, mSize, i, key);
mValues = CollectionUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
/**
* Returns the number of key-value mappings that this SparseILArray
* currently stores.
*/
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseILArray stores.
*
* <p>The keys corresponding to indices in ascending order are guaranteed to
* be in ascending order, e.g., <code>keyAt(0)</code> will return the
* smallest key and <code>keyAt(size()-1)</code> will return the largest
* key.</p>
*/
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseILArray stores.
*
* <p>The values corresponding to indices in ascending order are guaranteed
* to be associated with keys in ascending order, e.g.,
* <code>valueAt(0)</code> will return the value associated with the
* smallest key and <code>valueAt(size()-1)</code> will return the value
* associated with the largest key.</p>
*/
@SuppressWarnings("unchecked")
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, sets a new
* value for the <code>index</code>th key-value mapping that this
* SparseILArray stores.
*/
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* Returns an index for which {@link #valueAt} would return the
* specified key, or a negative number if no keys map to the
* specified value.
* <p>Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
* <p>Note also that unlike most collections' {@code indexOf} methods,
* this method compares values using {@code ==} rather than {@code equals}.
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
/**
* Removes all key-value mappings from this SparseILArray.
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
mKeys = CollectionUtils.append(mKeys, mSize, key);
mValues = CollectionUtils.append(mValues, mSize, value);
mSize++;
}
/**
* {@inheritDoc}
*
* <p>This implementation composes a string by iterating over its mappings. If
* this map contains itself as a value, the string "(this Map)"
* will appear in its place.
*/
@Override
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i = 0; i < mSize; i++) {
if (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
}
@@ -1,101 +0,0 @@
/*
* Copyright 2015-2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.yorozuya.thread;
import androidx.annotation.NonNull;
import java.util.Queue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SerialThreadExecutor implements Executor, Runnable {
private final Lock mThreadLock = new ReentrantLock();
private final Object mWaitLock = new Object();
private final long mKeepAliveMillis;
private final Queue<Runnable> mWorkQueue;
private final ThreadFactory mThreadFactory;
// The worker
private Thread mThread;
public SerialThreadExecutor(long keepAliveMillis,
Queue<Runnable> workQueue, ThreadFactory threadFactory) {
mKeepAliveMillis = keepAliveMillis;
mWorkQueue = workQueue;
mThreadFactory = threadFactory;
}
@Override
public void execute(@NonNull Runnable command) {
mThreadLock.lock();
mWorkQueue.add(command);
// Ensure thread
if (mThread == null) {
mThread = mThreadFactory.newThread(this);
mThread.start();
} else {
synchronized (mWaitLock) {
mWaitLock.notify();
}
}
mThreadLock.unlock();
}
@Override
public void run() {
// It tell whether has waited
boolean hasWaited = false;
for (; ; ) {
mThreadLock.lock();
Runnable runnable = mWorkQueue.poll();
if (runnable == null) {
if (hasWaited) {
// Have waited enough time
mThread = null;
mThreadLock.unlock();
break;
} else {
mThreadLock.unlock();
hasWaited = true;
synchronized (mWaitLock) {
try {
mWaitLock.wait(mKeepAliveMillis);
} catch (InterruptedException e) {
// Ignore
}
}
continue;
}
} else {
mThreadLock.unlock();
}
hasWaited = false;
try {
runnable.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?drawableColorSecondary"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
+1 -1
View File
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?drawableColorSecondary"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
+1 -1
View File
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?drawableColorSecondary"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?drawableColorSecondary"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?drawableColorThemePrimary"
android:tint="?colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
@@ -17,7 +17,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?drawableColorThemePrimary"
android:tint="?colorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff. Show more