Compare commits

..
1 Commits
Author SHA1 Message Date
txyyh e2cfa30052 Release 1.7.29.2
CI / Build (push) Has been cancelled
Releases / Build (push) Has been cancelled
2022-07-25 14:54:18 +08:00
219 changed files with 9815 additions and 5391 deletions

No files matched your search

+1 -1
View File
@@ -57,7 +57,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
-1
View File
@@ -1,4 +1,3 @@
blank_issues_enabled: false
contact_links:
- name: 所有其他问题 / All other questions
url: https://github.com/Ehviewer-Overhauled/Ehviewer/discussions
+1 -1
View File
@@ -56,7 +56,7 @@ body:
id: ci
attributes:
label: EhViewer CI 版本 / EhViewer CI Version
description: 直接将对应 GitHub Action CI 下载页面网址填入即可 / Simply drop GitHub Action CI download page url here
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
placeholder: https://github.com/Ehviewer-Overhauled/Ehviewer/actions/runs/XXXXXXXXXX
validations:
required: true
+7 -10
View File
@@ -1,11 +1,6 @@
name: CI
on:
push:
branches:
- '*'
tags-ignore:
- "*"
on: [push]
jobs:
linux:
@@ -14,8 +9,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
@@ -23,14 +16,18 @@ jobs:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- run: |
git submodule update --init --recursive
gradle assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
+6 -4
View File
@@ -9,8 +9,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
@@ -18,14 +16,18 @@ jobs:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- run: |
git submodule update --init --recursive
gradle assembleRelease
- name: Update ARM64
uses: actions/upload-artifact@v3
+11 -38
View File
@@ -12,8 +12,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: Setup Java 11
uses: actions/setup-java@v3
@@ -21,51 +19,26 @@ jobs:
distribution: temurin
java-version: 11
- name: Setup Android SDK Tools
uses: android-actions/setup-android@v2
- name: Install NDK
run: |
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
arguments: assembleRelease
- name: Rename Apks
run: |
cp app/build/outputs/apk/release/app-universal-release.apk app/build/outputs/apk/release/EhViewer-universal-A12+.apk
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
cp app/build/outputs/apk/release/app-x86_64-release.apk app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
cp app/build/outputs/apk/release/app-x86-release.apk app/build/outputs/apk/release/EhViewer-x86-A12+.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
- run: |
git submodule update --init --recursive
gradle assembleRelease
- name: Releases
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v0.1.14
with:
body: Bump Version
discussion_category_name: Announcements
files: |
app/build/outputs/apk/release/EhViewer-universal-A12+.apk
app/build/outputs/apk/release/EhViewer-arm64-v8a-A12+.apk
app/build/outputs/apk/release/EhViewer-x86_64-A12+.apk
app/build/outputs/apk/release/EhViewer-x86-A12+.apk
app/build/outputs/apk/release/EhViewer-armeabi-v7a-A12+.apk
- name: Collect Apks
run: |
mkdir app/build/outputs/apk/release/fdroid
cp app/build/outputs/apk/release/app-arm64-v8a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME.apk
cp app/build/outputs/apk/release/app-armeabi-v7a-release.apk app/build/outputs/apk/release/fdroid/EhViewer-v$GITHUB_REF_NAME-armv7.apk
- name: Push Apks
uses: cpina/github-action-push-to-another-repository@v1.5
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: app/build/outputs/apk/release/fdroid
destination-github-username: Ehviewer-Overhauled
destination-repository-name: fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: deploy
commit-message: Add apks from $GITHUB_REF
target-directory: fdroid/tmp-apks
app/build/outputs/apk/release/app-arm64-v8a-release.apk
app/build/outputs/apk/release/app-x86_64-release.apk
app/build/outputs/apk/release/app-x86-release.apk
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
+15 -6
View File
@@ -1,9 +1,18 @@
[submodule "app/src/main/cpp/libarchive"]
path = app/src/main/cpp/libarchive
[submodule "native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo"]
path = native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo
url = https://github.com/Ehviewer-Overhauled/libjpeg-turbo
[submodule "native/src/main/jni/image/giflib/giflib"]
path = native/src/main/jni/image/giflib/giflib
url = https://github.com/Ehviewer-Overhauled/giflib
[submodule "native/src/main/jni/image/libpng/libpng"]
path = native/src/main/jni/image/libpng/libpng
url = https://github.com/Ehviewer-Overhauled/libpng
[submodule "native/src/main/jni/libarchive"]
path = native/src/main/jni/libarchive
url = https://github.com/Ehviewer-Overhauled/libarchive
[submodule "app/src/main/cpp/liblzma"]
path = app/src/main/cpp/liblzma
[submodule "native/src/main/jni/liblzma"]
path = native/src/main/jni/liblzma
url = https://github.com/Ehviewer-Overhauled/xz
[submodule "app/src/main/cpp/nettle/nettle"]
path = app/src/main/cpp/nettle/nettle
[submodule "native/src/main/jni/nettle/nettle"]
path = native/src/main/jni/nettle/nettle
url = https://github.com/Ehviewer-Overhauled/nettle
+21 -45
View File
@@ -1,17 +1,5 @@
<p align="right">
<strong>English<strong>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-cn.md">
简体中文
</a>
<span> | <span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/docs/README/zh-tw.md">
正體中文
</a>
</p>
<h1 align="center">
<img src="art/launcher_icon-web.svg" width="200">
<img src="art/launcher_icon-web.png" width="200">
<br>EhViewer<br>
</h1>
@@ -19,7 +7,7 @@
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml">
<img src="https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
</a>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/1.8.x.x/LICENSE">
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/Ehviewer-Overhauled/Ehviewer" alt="LICENSE">
</a>
<a href="https://www.codefactor.io/repository/github/Ehviewer-Overhauled/ehviewer">
@@ -33,42 +21,22 @@
</a>
</p>
<div align="center">
<h3>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#description">
Description
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#download">
Download
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#screenshot">
Screenshot
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#thanks">
Thanks
</a>
<span> | </span>
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer#license">
License
</a>
</h3>
</div>
# Description
致力于轻量化与良好性能表现的 EhViewer 分支
已针对性适配 Material Design 3 及其动态取色系统
EhViewer fork dedicated to lightweight and high-performance
with Material Design 3 and Dynamic Color Support
# Download
<a href="https://github.com/Ehviewer-Overhauled/Ehviewer/releases"><img alt="Get it on GitHub" src="art/get-it-on-github.svg" width="200px"/></a> <a href="https://github.com/Ehviewer-Overhauled/fdroid-repo"><img alt="Git it on F-Droid" src="art/get-it-on-fdroid.svg" width="200px"/></a>
[Github 下载](https://github.com/Ehviewer-Overhauled/Ehviewer/releases)
App is divided into two versions: 1.7 and 1.8 . Version 1.8 support Android 12 and above, also the develop mainline. Version 1.7 support Android 11 and below, drop down some spicetify functions for Android 12 to make app useable on lower Android System
[CI 版本下载(需登录)](https://github.com/Ehviewer-Overhauled/Ehviewer/actions/workflows/ci.yml)
# Screenshot
@@ -77,16 +45,24 @@ App is divided into two versions: 1.7 and 1.8 . Version 1.8 support Android 12 a
# Thanks
本项目受到了诸多开源项目的帮助
Here is the libraries
- [AOSP&AndroidX](http://source.android.com/)
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
- [GnuTLS](https://gnutls.org/)
- [AOSP](http://source.android.com/)
- [android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
- [material-components-android](https://github.com/material-components/material-components-android)
- [material-design-icons](https://github.com/google/material-design-icons)
- [Apache Commons Lang](https://commons.apache.org/proper/commons-lang/)
- [apng](http://apng.sourceforge.net/)
- [giflib](http://giflib.sourceforge.net)
- [greenDAO](https://github.com/greenrobot/greenDAO)
- [jsoup](https://github.com/jhy/jsoup)
- [libjpeg-turbo](https://libjpeg-turbo.org/)
- [libpng](http://www.libpng.org/pub/png/libpng.html)
- [okhttp](https://github.com/square/okhttp)
- [RikkaX](https://github.com/RikkaApps/RikkaX)
- [roaster](https://github.com/forge/roaster)
- [TapTargetView](https://github.com/KeepSafe/TapTargetView)
- [libarchive](http://www.libarchive.org/)
- [XZ Utils](https://tukaani.org/xz/)
+1 -2
View File
@@ -4,5 +4,4 @@ manifest-merger-release-report.txt
/src/main/libs
/src/main/obj
# Intellij
*.iml
/.cxx
*.iml
+29 -61
View File
@@ -1,13 +1,12 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
id 'kotlin-kapt'
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
ndkVersion "25.0.8775105"
Properties localProperties = new Properties()
if (project.rootProject.file('local.properties').exists()) {
@@ -19,7 +18,6 @@ android {
enable = true
reset()
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
universalApk = true
}
}
@@ -34,19 +32,13 @@ android {
defaultConfig {
applicationId "moe.tarsin.ehviewer"
minSdkVersion 31
minSdkVersion 26
targetSdkVersion 33
versionCode 180009
versionName "1.8.2.0"
versionCode 114534
versionName "1.7.29.2"
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
}
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
@@ -86,25 +78,27 @@ android {
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.3.0"
sourceSets {
main {
java.srcDirs += 'src/main/java-gen'
}
}
namespace 'com.hippo.ehviewer'
}
tasks.withType(JavaCompile) {
task -> task.dependsOn ":daogenerator:executeDaoGenerator"
}
dependencies {
def activity_version = "1.6.0-beta01"
def activity_version = "1.6.0-alpha05"
implementation "androidx.activity:activity:$activity_version"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.annotation:annotation:1.5.0-alpha02"
implementation "androidx.annotation:annotation-experimental:1.3.0-beta01"
implementation "androidx.appcompat:appcompat:1.6.0-beta01"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.annotation:annotation-experimental:1.2.0"
implementation "androidx.appcompat:appcompat:1.6.0-alpha05"
def arch_core_version = "2.1.0"
implementation "androidx.arch.core:core-common:$arch_core_version"
@@ -113,36 +107,27 @@ dependencies {
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
implementation "androidx.browser:browser:1.4.0"
def collection_version = "1.3.0-alpha02"
def collection_version = "1.3.0-alpha01"
implementation "androidx.collection:collection:$collection_version"
implementation "androidx.collection:collection-ktx:$collection_version"
def compose_version = "1.3.0-alpha03"
implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-saveable:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.0.0-alpha16"
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha03"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
def core_version = "1.9.0-beta01"
def core_version = "1.9.0-alpha05"
implementation "androidx.core:core:$core_version"
implementation "androidx.core:core-ktx:$core_version"
implementation "androidx.customview:customview:1.2.0-alpha01"
implementation "androidx.documentfile:documentfile:1.1.0-alpha01"
implementation "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03"
def fragment_version = "1.6.0-alpha01"
def fragment_version = "1.5.0"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
def lifecycle_version = "2.6.0-alpha01"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-core-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
@@ -150,23 +135,8 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
implementation "androidx.loader:loader:1.1.0"
def nav_version = "2.5.1"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.paging:paging-runtime:3.2.0-alpha02"
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta02'
def room_version = "2.5.0-alpha02"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation 'androidx.recyclerview:recyclerview:1.3.0-beta01'
implementation 'androidx.startup:startup-runtime:1.2.0-alpha01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation "androidx.transition:transition-ktx:1.4.1"
@@ -174,26 +144,24 @@ 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-rc01'
implementation 'androidx.webkit:webkit:1.5.0-beta01'
implementation 'com.drakeet.drawer:drawer:1.0.3'
implementation 'com.google.android.material:material:1.7.0-beta01'
implementation 'com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.google.android.material:material:1.7.0-alpha03'
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.greenrobot:greendao:3.3.0'
implementation 'org.jsoup:jsoup:1.15.2'
implementation projects.native
}
configurations.all {
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
exclude group: 'dev.rikka.rikkax.material', module: 'material'
exclude group: 'androidx.compose.material'
}
+17 -8
View File
@@ -7,19 +7,28 @@
public static void throw*(...);
}
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
-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 { }
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BaseFragment { }
-keep class com.hippo.ehviewer.ui.fragment.* extends com.hippo.ehviewer.ui.fragment.BasePreferenceFragment { }
-keepnames class com.hippo.ehviewer.ui.scene.* { }
-keepattributes SourceFile,LineNumberTable
-keep class com.hippo.ehviewer.dao.* { *; }
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute
-repackageclasses
-allowaccessmodification
-mergeinterfacesaggressively
-overloadaggressively
-dontwarn net.sqlcipher.database.**
-dontwarn rx.**
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn net.sqlcipher.Cursor
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
+4 -13
View File
@@ -23,16 +23,14 @@
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Required to maintain app compatibility. -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
@@ -47,7 +45,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
@@ -147,12 +144,6 @@
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.ui.SecurityActivity"
android:configChanges="screenSize"
android:label="@string/settings"
android:theme="@style/AppTheme" />
<activity
android:name="com.hippo.ehviewer.shortcuts.ShortcutsActivity"
android:excludeFromRecents="true"
-339
View File
@@ -1,339 +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/>.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <jni.h>
#include <android/log.h>
#include <archive.h>
#include <archive_entry.h>
#define LOG_TAG "libarchive_wrapper"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)
typedef struct {
int using;
int next_index;
struct archive *arc;
struct archive_entry *entry;
} archive_ctx;
pthread_mutex_t ctx_lock;
static archive_ctx **ctx_pool;
#define CTX_POOL_SIZE 20
static bool need_encrypt = false;
static char *passwd = NULL;
static void *archiveAddr = NULL;
static jlong archiveSize = 0;
const char supportExt[9][6] = {
"jpeg",
"jpg",
"png",
"gif",
"webp",
"bmp",
"ico",
"wbmp",
"heif"
};
static inline int filename_is_playable_file(const char *name) {
const char *dotptr = strrchr(name, '.');
if (!dotptr++)
return false;
int i;
for (i = 0; i < 9; i++)
if (strcmp(dotptr, supportExt[i]) == 0)
return true;
return false;
}
static long archive_list_all_entries(archive_ctx *ctx) {
long count = 0;
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
count++;
if (!count)
LOGE("%s", archive_error_string(ctx->arc));
return count;
}
static void archive_release_ctx(archive_ctx *ctx) {
if (ctx) {
archive_read_close(ctx->arc);
archive_read_free(ctx->arc);
free(ctx);
}
}
static int archive_alloc_ctx(archive_ctx **ctxptr) {
archive_ctx *ctx;
int err;
ctx = calloc(1, sizeof(archive_ctx));
if (!ctx)
return -ENOMEM;
ctx->arc = archive_read_new();
ctx->using = 1;
if (!ctx->arc) {
free(ctx);
return -ENOMEM;
}
archive_read_support_format_all(ctx->arc);
archive_read_support_filter_all(ctx->arc);
archive_read_set_option(ctx->arc, "zip", "ignorecrc32", "1");
if (passwd)
archive_read_add_passphrase(ctx->arc, passwd);
err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);
if (err) {
archive_read_free(ctx->arc);
free(ctx);
return err;
}
*ctxptr = ctx;
return 0;
}
static int archive_skip_to_index(archive_ctx *ctx, int index) {
assert(!(ctx->next_index > index));
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
continue;
if (ctx->next_index++ == index) {
return ctx->next_index - 1;
}
}
return ARCHIVE_FATAL;
}
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
int ret;
archive_ctx *ctx = NULL;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i])
continue;
if (ctx_pool[i]->using)
continue;
if (ctx_pool[i]->next_index > idx)
continue;
if (!ctx || ctx_pool[i]->next_index > ctx->next_index)
ctx = ctx_pool[i];
if (ctx->next_index == idx)
break;
}
if (ctx)
ctx->using = 1;
pthread_mutex_unlock(&ctx_lock);
if (!ctx) {
archive_ctx *victimCtx = NULL;
int victimIdx = 0;
int replace = 1;
ret = archive_alloc_ctx(&ctx);
if (ret)
return ret;
pthread_mutex_lock(&ctx_lock);
for (int i = 0; i < CTX_POOL_SIZE; i++) {
if (!ctx_pool[i]) {
ctx_pool[i] = ctx;
replace = 0;
break;
}
if (ctx_pool[i]->using)
continue;
if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {
victimCtx = ctx_pool[i];
victimIdx = i;
}
}
if (replace) {
archive_release_ctx(victimCtx);
ctx_pool[victimIdx] = ctx;
}
pthread_mutex_unlock(&ctx_lock);
}
ret = archive_skip_to_index(ctx, idx);
if (ret != idx) {
ret = archive_errno(ctx->arc);
LOGE("Skip to index failed:%s", archive_error_string(ctx->arc));
archive_release_ctx(ctx);
return ret;
}
*ctxptr = ctx;
return 0;
}
JNIEXPORT jint JNICALL
Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd, jlong size) {
(void)env; /* UNUSED */
(void)thiz; /* UNUSED */
archive_ctx *ctx = NULL;
archiveAddr = mmap64(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (archiveAddr == MAP_FAILED) {
LOGE("%s%d", "mmap64 failed with errno ", errno);
return 0;
}
archiveSize = size;
pthread_mutex_init(&ctx_lock, 0);
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
if (!ctx_pool) {
LOGE("Allocate archive ctx pool failed:ENOMEM");
return 0;
}
long r = archive_alloc_ctx(&ctx);
if (r) {
if (r == -ENOMEM)
LOGE("%s", "Mem alloc failed");
r = 0;
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
} else {
r = archive_list_all_entries(ctx);
LOGI("%s%ld%s", "Found ", r, " image entries in archive");
// We must read through the file|vm then we can know whether it is encrypted
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
switch (encryptRet) {
case 1: // At lease 1 encrypted entry
need_encrypt = true;
break;
case 0: // format supports but no encrypted entry found
default:
need_encrypt = false;
}
// madvise for readahead optimization
int format = archive_format(ctx->arc);
switch (format) {
case ARCHIVE_FORMAT_ZIP:
case ARCHIVE_FORMAT_RAR:
case ARCHIVE_FORMAT_RAR_V5:
madvise(archiveAddr, archiveSize, MADV_SEQUENTIAL | MADV_WILLNEED);
break;
default:;
}
}
archive_release_ctx(ctx);
return r;
}
typedef struct Memarea {
void *buffer;
long size;
} Memarea;
JNIEXPORT jlong JNICALL
Java_com_hippo_UriArchiveAccessor_extracttoOutputStream(JNIEnv *env, jobject thiz, jint index) {
(void)env; /* UNUSED */
(void)thiz; /* UNUSED */
archive_ctx *ctx = NULL;
int ret;
ret = archive_get_ctx(&ctx, index);
if (ret)
return 0;
Memarea *memarea = malloc(sizeof(Memarea));
if (!memarea) {
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
return 0;
}
memarea->size = (long) archive_entry_size(ctx->entry);
memarea->buffer = malloc(memarea->size);
if (!memarea->buffer) {
ctx->using = 0;
LOGE("Allocate buffer for decompression failed:ENOMEM");
free(memarea);
return 0;
}
ret = archive_read_data(ctx->arc, memarea->buffer, memarea->size);
ctx->using = 0;
if (ret == memarea->size)
return (jlong) memarea;
if (ret != memarea->size)
LOGE("%s", "No enough data read, WTF?");
if (ret < 0)
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
free(memarea->buffer);
free(memarea);
return 0;
}
JNIEXPORT void JNICALL
Java_com_hippo_UriArchiveAccessor_closeArchive(JNIEnv *env, jobject thiz) {
(void)env; /* UNUSED */
(void)thiz; /* UNUSED */
for (int i = 0; i < CTX_POOL_SIZE; i++)
archive_release_ctx(ctx_pool[i]);
free(passwd);
free(ctx_pool);
pthread_mutex_destroy(&ctx_lock);
passwd = NULL;
need_encrypt = false;
munmap(archiveAddr, archiveSize);
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_needPassword(JNIEnv *env, jobject thiz) {
(void)env; /* UNUSED */
(void)thiz; /* UNUSED */
return need_encrypt;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_UriArchiveAccessor_providePassword(JNIEnv *env, jobject thiz, jstring str) {
(void)thiz; /* UNUSED */
struct archive_entry *entry;
archive_ctx *ctx;
jboolean ret = true;
int len = (*env)->GetStringUTFLength(env, str);
passwd = realloc(passwd, len * sizeof(char));
if (!passwd) {
LOGE("Allocate passwd buffer failed");
return false;
}
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
archive_alloc_ctx(&ctx);
void *tmpBuf = alloca(4096);
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
if (!filename_is_playable_file(archive_entry_pathname(entry)))
continue;
if (!archive_entry_is_encrypted(entry))
continue;
if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
ret = false;
}
break;
}
archive_release_ctx(ctx);
return ret;
}
-380
View File
@@ -1,380 +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/>.
*/
#include <stdlib.h>
#include <string.h>
#include <android/data_space.h>
#include <android/imagedecoder.h>
#include <android/log.h>
#include <GLES2/gl2.h>
#include "image.h"
#define TAG "ImageDecoder_wrapper"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG ,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, TAG ,__VA_ARGS__)
#define IMAGE_TILE_MAX_SIZE (512 * 512)
static void *tile_buffer = NULL;
bool image_onLoad() {
tile_buffer = malloc(IMAGE_TILE_MAX_SIZE * 4);
return tile_buffer != NULL;
}
void image_onUnload() {
free(tile_buffer);
tile_buffer = NULL;
}
typedef struct {
AImageDecoder *decoder;
void *srcBuffer;
size_t stride;
int fd;
} SOURCE;
typedef struct {
int32_t width;
int32_t height;
void *buffer;
int bufferLen;
bool isAnimated;
bool alpha;
int delay;
SOURCE *src;
} IMAGE;
static void recycleSource(SOURCE *src) {
if (!src)
return;
AImageDecoder_delete(src->decoder);
free(src->srcBuffer);
free(src);
}
static void recycle(IMAGE *image) {
if (!image)
return;
recycleSource(image->src);
free(image->buffer);
free(image);
}
typedef struct Memarea {
void *buffer;
long size;
} Memarea;
bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
int left;
int line;
size_t line_stride;
int src_stride;
int src_pos;
int dst_pos;
size_t dst_blank_length;
// Sanitize
if (src_x < 0) {
width -= src_x;
dst_x -= src_x;
src_x = 0;
}
if (dst_x < 0) {
width -= dst_x;
src_x -= dst_x;
dst_x = 0;
}
if (width <= 0) {
return false;
}
if (src_y < 0) {
height -= src_y;
dst_y -= src_y;
src_y = 0;
}
if (dst_y < 0) {
height -= dst_y;
src_y -= dst_y;
dst_y = 0;
}
if (height <= 0) {
return false;
}
left = src_x + width - src_w;
if (left > 0) {
width -= left;
}
left = dst_x + width - dst_w;
if (left > 0) {
width -= left;
}
if (width <= 0) {
return false;
}
left = src_y + height - src_h;
if (left > 0) {
height -= left;
}
left = dst_y + height - dst_h;
if (left > 0) {
height -= left;
}
if (height <= 0) {
return false;
}
// Init
line_stride = (size_t) (width * 4);
src_stride = src_w * 4;
src_pos = src_y * src_stride + src_x * 4;
dst_pos = 0;
dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * 4;
// First line
dst_pos += (int)dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int)line_stride;
src_pos += src_stride;
// Other lines
dst_blank_length = (size_t) ((dst_w - width) * 4);
for (line = 1; line < height; line++) {
dst_pos += (int)dst_blank_length;
memcpy(dst + dst_pos, src + src_pos, line_stride);
dst_pos += (int)line_stride;
src_pos += src_stride;
}
return true;
}
void decodeCommon(IMAGE *image, SOURCE *src) {
image->src = src;
AImageDecoder *decoder = src->decoder;
image->isAnimated = AImageDecoder_isAnimated(decoder);
AImageDecoder_setDataSpace(decoder, ADATASPACE_SRGB);
AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888);
const AImageDecoderHeaderInfo *headerInfo = AImageDecoder_getHeaderInfo(decoder);
image->height = AImageDecoderHeaderInfo_getHeight(headerInfo);
image->width = AImageDecoderHeaderInfo_getWidth(headerInfo);
image->alpha =
AImageDecoderHeaderInfo_getAlphaFlags(headerInfo) == ANDROID_BITMAP_FLAGS_ALPHA_PREMUL;
src->stride = AImageDecoder_getMinimumStride(decoder);
image->bufferLen = (int) (image->height * src->stride);
image->buffer = malloc(image->bufferLen);
if (image->isAnimated) {
AImageDecoder_setInternallyHandleDisposePrevious(decoder, false);
AImageDecoderFrameInfo *frameInfo = AImageDecoderFrameInfo_create();
AImageDecoder_getFrameInfo(decoder, frameInfo);
image->delay = (int) AImageDecoderFrameInfo_getDuration(frameInfo) / 1000000;
AImageDecoderFrameInfo_delete(frameInfo);
}
AImageDecoder_decodeImage(decoder, image->buffer, src->stride, image->bufferLen);
if (!image->isAnimated) {
recycleSource(image->src);
image->src = NULL;
}
}
IMAGE *createFromFd(int fd) {
AImageDecoder *decoder = NULL;
if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS)
return NULL;
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->fd = fd;
decodeCommon(image, src);
return image;
}
IMAGE *createFromAddr(void *addr, long size) {
AImageDecoder *decoder;
AImageDecoder_createFromBuffer((const void *) addr, size, &decoder);
IMAGE *image = calloc(1, sizeof(IMAGE));
SOURCE *src = calloc(1, sizeof(SOURCE));
src->decoder = decoder;
src->srcBuffer = addr;
decodeCommon(image, src);
return image;
}
IMAGE *create(int32_t width, int32_t height, const void *data) {
IMAGE *plain = calloc(1, sizeof(IMAGE));
void *buffer = NULL;
size_t length = width * height * 4;
buffer = malloc(length);
memcpy(buffer, data, length);
// plain->isAnimated = false; this is unnecessary since we use calloc
plain->width = width;
plain->height = height;
plain->buffer = buffer;
plain->bufferLen = (int) length;
return plain;
}
void render(IMAGE *image, int src_x, int src_y,
void *dst, int dst_w, int dst_h, int dst_x, int dst_y,
int width, int height) {
copy_pixels(image->buffer, image->width, image->height, src_x, src_y, dst, dst_w, dst_h, dst_x,
dst_y, width, height);
}
void advance(IMAGE *image) {
if (image->isAnimated) {
AImageDecoder_advanceFrame(image->src->decoder);
if (AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen) == ANDROID_IMAGE_DECODER_FINISHED) {
AImageDecoder_rewind(image->src->decoder);
AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride,
image->bufferLen);
}
}
}
static int jniGetFDFromFileDescriptor(JNIEnv *env, jobject fileDescriptor) {
jint fd = -1;
jclass fdClass = (*env)->FindClass(env, "java/io/FileDescriptor");
if (fdClass != NULL) {
jfieldID fdClassDescriptorFieldID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
if (fdClassDescriptorFieldID != NULL && fileDescriptor != NULL) {
fd = (*env)->GetIntField(env, fileDescriptor, fdClassDescriptorFieldID);
}
}
return fd;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecodeFdInt(JNIEnv *env, jclass clazz, jint fd) {
IMAGE *image = createFromFd(fd);
return (jlong) image;
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecode(JNIEnv *env, jclass clazz, jobject fd) {
return Java_com_hippo_image_Image_nativeDecodeFdInt(env, clazz,
jniGetFDFromFileDescriptor(env, fd));
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeCreate(JNIEnv *env, jclass clazz, jobject bitmap) {
AndroidBitmapInfo info;
void *pixels = NULL;
void *image = NULL;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
image = create((int)info.width, (int)info.height, pixels);
AndroidBitmap_unlockPixels(env, bitmap);
return (jlong) image;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jlong ptr, jint src_x,
jint src_y, jobject dst, jint dst_x, jint dst_y, jint width,
jint height) {
AndroidBitmapInfo info;
void *pixels = NULL;
AndroidBitmap_getInfo(env, dst, &info);
AndroidBitmap_lockPixels(env, dst, &pixels);
render((IMAGE *) ptr, src_x, src_y, pixels, (int)info.width, (int)info.height, dst_x, dst_y, width,
height);
AndroidBitmap_unlockPixels(env, dst);
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr, jboolean init,
jint src_x, jint src_y, jint width, jint height) {
if (width * height > IMAGE_TILE_MAX_SIZE)
return;
render((IMAGE *) ptr, src_x, src_y, tile_buffer, width, height, 0, 0, width, height);
if (init) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE,
tile_buffer);
}
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeAdvance(JNIEnv *env, jclass clazz, jlong ptr) {
advance((IMAGE *) ptr);
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetDelay(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->delay;
}
JNIEXPORT jboolean JNICALL
Java_com_hippo_image_Image_nativeIsOpaque(JNIEnv *env, jclass clazz, jlong ptr) {
return ((IMAGE *) ptr)->alpha;
}
JNIEXPORT void JNICALL
Java_com_hippo_image_Image_nativeRecycle(JNIEnv *env, jclass clazz, jlong ptr) {
recycle((IMAGE *) ptr);
}
JNIEXPORT jlong JNICALL
Java_com_hippo_image_Image_nativeDecodeAddr(JNIEnv *env, jclass clazz, jlong addr) {
Memarea *memarea = (Memarea *) addr;
IMAGE *image = createFromAddr(memarea->buffer, memarea->size);
free(memarea);
return (jlong) image;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetFormat(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->isAnimated;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetWidth(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->width;
}
JNIEXPORT jint JNICALL
Java_com_hippo_image_Image_nativeGetHeight(JNIEnv *env, jclass clazz, jlong native_ptr) {
return ((IMAGE *) native_ptr)->height;
}
@@ -167,7 +167,6 @@ public final class DiskLruCache implements Closeable {
* its sequence number is not equal to its entry's sequence number.
*/
private long nextSequenceNumber = 0;
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
@@ -176,7 +175,21 @@ public final class DiskLruCache implements Closeable {
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
/**
* Opens the cache in {@code directory}, creating a cache if none exists
@@ -237,21 +250,7 @@ public final class DiskLruCache implements Closeable {
if (file.exists() && !file.delete()) {
throw new IOException();
}
} private final Callable<Void> cleanupCallable = new Callable<>() {
public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
}
return null;
}
};
}
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
if (deleteDestination) {
@@ -26,7 +26,6 @@ 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;
@@ -61,7 +60,6 @@ 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.Image;
import com.hippo.image.ImageBitmap;
import com.hippo.network.StatusCodeException;
@@ -109,7 +107,7 @@ public class EhApplication extends SceneApplication {
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
private static final long DEBUG_PRINT_INTERVAL = 3000L;
public static boolean locked = true;
public static boolean locked = false;
private static EhApplication instance;
@@ -339,10 +337,11 @@ public class EhApplication extends SceneApplication {
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()));
if (EhDB.needMerge()) {
EhDB.mergeOldDB(this);
}
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
// Do io tasks in new thread
@@ -533,9 +532,9 @@ public class EhApplication extends SceneApplication {
}
@Nullable
public EhActivity getTopActivity() {
public Activity getTopActivity() {
if (!mActivityList.isEmpty()) {
return (EhActivity) mActivityList.get(mActivityList.size() - 1);
return mActivityList.get(mActivityList.size() - 1);
} else {
return null;
}
+384 -98
View File
@@ -17,24 +17,27 @@
package com.hippo.ehviewer;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.room.Room;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.BasicDao;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.ehviewer.dao.DaoMaster;
import com.hippo.ehviewer.dao.DaoSession;
import com.hippo.ehviewer.dao.DownloadDirname;
import com.hippo.ehviewer.dao.DownloadDirnameDao;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.DownloadLabelDao;
import com.hippo.ehviewer.dao.DownloadsDao;
import com.hippo.ehviewer.dao.EhDatabase;
import com.hippo.ehviewer.dao.Filter;
import com.hippo.ehviewer.dao.FilterDao;
import com.hippo.ehviewer.dao.HistoryDao;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.dao.LocalFavoriteInfo;
@@ -43,8 +46,14 @@ import com.hippo.ehviewer.dao.QuickSearch;
import com.hippo.ehviewer.dao.QuickSearchDao;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.util.ExceptionUtils;
import com.hippo.util.SqlUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.collect.SparseJLArray;
import org.greenrobot.greendao.AbstractDao;
import org.greenrobot.greendao.query.CloseableListIterator;
import org.greenrobot.greendao.query.LazyList;
import java.io.File;
import java.io.FileInputStream;
@@ -56,9 +65,15 @@ import java.util.ArrayList;
import java.util.List;
public class EhDB {
private static final int CUR_DB_VER = 4;
private static EhDatabase db;
private static final String TAG = EhDB.class.getSimpleName();
private static final int MAX_HISTORY_COUNT = 200;
private static DaoSession sDaoSession;
private static boolean sHasOldDB;
private static boolean sNewDB;
private static void upgradeDB(SQLiteDatabase db, int oldVersion) {
switch (oldVersion) {
@@ -100,12 +115,219 @@ public class EhDB {
}
public static void initialize(Context context) {
db = Room.databaseBuilder(context, EhDatabase.class, "eh.db").allowMainThreadQueries().build();
sHasOldDB = context.getDatabasePath("data").exists();
DBOpenHelper helper = new DBOpenHelper(
context.getApplicationContext(), "eh.db", null);
SQLiteDatabase db = helper.getWritableDatabase();
DaoMaster daoMaster = new DaoMaster(db);
sDaoSession = daoMaster.newSession();
}
public static boolean needMerge() {
return sNewDB && sHasOldDB;
}
public static void mergeOldDB(Context context) {
sNewDB = false;
OldDBHelper oldDBHelper = new OldDBHelper(context);
SQLiteDatabase oldDB;
try {
oldDB = oldDBHelper.getReadableDatabase();
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
return;
}
// Get GalleryInfo list
SparseJLArray<GalleryInfo> map = new SparseJLArray<>();
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_GALLERY, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
GalleryInfo gi = new GalleryInfo();
gi.gid = cursor.getInt(0);
gi.token = cursor.getString(1);
gi.title = cursor.getString(2);
gi.posted = cursor.getString(3);
gi.category = cursor.getInt(4);
gi.thumb = cursor.getString(5);
gi.uploader = cursor.getString(6);
try {
// In 0.6.x version, NaN is stored
gi.rating = cursor.getFloat(7);
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
gi.rating = -1.0f;
}
map.put(gi.gid, gi);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge local favorites
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_LOCAL_FAVOURITE, null);
if (cursor != null) {
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
if (cursor.moveToFirst()) {
long i = 0L;
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
LocalFavoriteInfo info = new LocalFavoriteInfo(gi);
info.setTime(i);
dao.insert(info);
cursor.moveToNext();
i++;
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge quick search
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_TAG, null);
if (cursor != null) {
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
QuickSearch quickSearch = new QuickSearch();
int mode = cursor.getInt(2);
String search = cursor.getString(4);
String tag = cursor.getString(7);
if (mode == ListUrlBuilder.MODE_UPLOADER && search != null &&
search.startsWith("uploader:")) {
search = search.substring("uploader:".length());
}
quickSearch.setTime(cursor.getInt(0));
quickSearch.setName(cursor.getString(1));
quickSearch.setMode(mode);
quickSearch.setCategory(cursor.getInt(3));
quickSearch.setKeyword(mode == ListUrlBuilder.MODE_TAG ? tag : search);
quickSearch.setAdvanceSearch(cursor.getInt(5));
quickSearch.setMinRating(cursor.getInt(6));
dao.insert(quickSearch);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
// Merge download info
try {
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_DOWNLOAD, null);
if (cursor != null) {
DownloadsDao dao = sDaoSession.getDownloadsDao();
if (cursor.moveToFirst()) {
long i = 0L;
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
DownloadInfo info = new DownloadInfo(gi);
int state = cursor.getInt(2);
int legacy = cursor.getInt(3);
if (state == DownloadInfo.STATE_FINISH && legacy > 0) {
state = DownloadInfo.STATE_FAILED;
}
info.setState(state);
info.setLegacy(legacy);
if (cursor.getColumnCount() == 5) {
info.setTime(cursor.getLong(4));
} else {
info.setTime(i);
}
dao.insert(info);
cursor.moveToNext();
i++;
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
try {
// Merge history info
Cursor cursor = oldDB.rawQuery("select * from " + OldDBHelper.TABLE_HISTORY, null);
if (cursor != null) {
HistoryDao dao = sDaoSession.getHistoryDao();
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
// Get GalleryInfo first
long gid = cursor.getInt(0);
GalleryInfo gi = map.get(gid);
if (gi == null) {
Log.e(TAG, "Can't get GalleryInfo with gid: " + gid);
cursor.moveToNext();
continue;
}
HistoryInfo info = new HistoryInfo(gi);
info.setMode(cursor.getInt(1));
info.setTime(cursor.getLong(2));
dao.insert(info);
cursor.moveToNext();
}
}
cursor.close();
}
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
try {
oldDBHelper.close();
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
// Ignore
}
}
public static synchronized List<DownloadInfo> getAllDownloadInfo() {
DownloadsDao dao = db.downloadsDao();
List<DownloadInfo> list = dao.list();
DownloadsDao dao = sDaoSession.getDownloadsDao();
List<DownloadInfo> list = dao.queryBuilder().orderDesc(DownloadsDao.Properties.Time).list();
// Fix state
for (DownloadInfo info : list) {
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
@@ -117,7 +339,7 @@ public class EhDB {
// Insert or update
public static synchronized void putDownloadInfo(DownloadInfo downloadInfo) {
DownloadsDao dao = db.downloadsDao();
DownloadsDao dao = sDaoSession.getDownloadsDao();
if (null != dao.load(downloadInfo.gid)) {
// Update
dao.update(downloadInfo);
@@ -127,13 +349,13 @@ public class EhDB {
}
}
public static synchronized void removeDownloadInfo(DownloadInfo downloadInfo) {
db.downloadsDao().delete(downloadInfo);
public static synchronized void removeDownloadInfo(long gid) {
sDaoSession.getDownloadsDao().deleteByKey(gid);
}
@Nullable
public static synchronized String getDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) {
return raw.getDirname();
@@ -146,7 +368,7 @@ public class EhDB {
* Insert or update
*/
public static synchronized void putDownloadDirname(long gid, String dirname) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
DownloadDirname raw = dao.load(gid);
if (raw != null) { // Update
raw.setDirname(dirname);
@@ -160,23 +382,23 @@ public class EhDB {
}
public static synchronized void removeDownloadDirname(long gid) {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
dao.deleteByKey(gid);
}
public static synchronized void clearDownloadDirname() {
DownloadDirnameDao dao = db.downloadDirnameDao();
DownloadDirnameDao dao = sDaoSession.getDownloadDirnameDao();
dao.deleteAll();
}
@NonNull
public static synchronized List<DownloadLabel> getAllDownloadLabelList() {
DownloadLabelDao dao = db.downloadLabelDao();
return dao.list();
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
return dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time).list();
}
public static synchronized DownloadLabel addDownloadLabel(String label) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
DownloadLabel raw = new DownloadLabel();
raw.setLabel(label);
raw.setTime(System.currentTimeMillis());
@@ -187,13 +409,13 @@ public class EhDB {
public static synchronized DownloadLabel addDownloadLabel(DownloadLabel raw) {
// Reset id
raw.setId(null);
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
raw.setId(dao.insert(raw));
return raw;
}
public static synchronized void updateDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
dao.update(raw);
}
@@ -206,8 +428,9 @@ public class EhDB {
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
DownloadLabelDao dao = db.downloadLabelDao();
List<DownloadLabel> list = dao.list(offset, limit);
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
List<DownloadLabel> list = dao.queryBuilder().orderAsc(DownloadLabelDao.Properties.Time)
.offset(offset).limit(limit).list();
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
@@ -218,44 +441,50 @@ public class EhDB {
}
list.get(start).setTime(toTime);
dao.update(list);
dao.updateInTx(list);
}
public static synchronized void removeDownloadLabel(DownloadLabel raw) {
DownloadLabelDao dao = db.downloadLabelDao();
DownloadLabelDao dao = sDaoSession.getDownloadLabelDao();
dao.delete(raw);
}
public static synchronized List<GalleryInfo> getAllLocalFavorites() {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time).list();
List<GalleryInfo> result = new ArrayList<>();
result.addAll(list);
return result;
}
public static synchronized List<GalleryInfo> searchLocalFavorites(String query) {
LocalFavoritesDao dao = db.localFavoritesDao();
List<LocalFavoriteInfo> list = dao.list();
return new ArrayList<>(list);
query = SqlUtils.sqlEscapeString("%" + query + "%");
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
List<LocalFavoriteInfo> list = dao.queryBuilder().orderDesc(LocalFavoritesDao.Properties.Time)
.where(LocalFavoritesDao.Properties.Title.like(query)).list();
List<GalleryInfo> result = new ArrayList<>();
result.addAll(list);
return result;
}
public static synchronized void removeLocalFavorites(long gid) {
db.localFavoritesDao().deleteByKey(gid);
sDaoSession.getLocalFavoritesDao().deleteByKey(gid);
}
public static synchronized void removeLocalFavorites(long[] gidArray) {
LocalFavoritesDao dao = db.localFavoritesDao();
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
for (long gid : gidArray) {
dao.deleteByKey(gid);
}
}
public static synchronized boolean containLocalFavorites(long gid) {
LocalFavoritesDao dao = db.localFavoritesDao();
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
return null != dao.load(gid);
}
public static synchronized void putLocalFavorites(GalleryInfo galleryInfo) {
LocalFavoritesDao dao = db.localFavoritesDao();
LocalFavoritesDao dao = sDaoSession.getLocalFavoritesDao();
if (null == dao.load(galleryInfo.gid)) {
LocalFavoriteInfo info;
if (galleryInfo instanceof LocalFavoriteInfo) {
@@ -275,31 +504,31 @@ public class EhDB {
}
public static synchronized List<QuickSearch> getAllQuickSearch() {
QuickSearchDao dao = db.quickSearchDao();
return dao.list();
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
return dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time).list();
}
public static synchronized void insertQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
quickSearch.id = null;
quickSearch.time = System.currentTimeMillis();
quickSearch.id = dao.insert(quickSearch);
}
public static synchronized void importQuickSearch(List<QuickSearch> quickSearchList) {
QuickSearchDao dao = db.quickSearchDao();
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
for (QuickSearch quickSearch : quickSearchList) {
dao.insert(quickSearch);
}
}
public static synchronized void updateQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
dao.update(quickSearch);
}
public static synchronized void deleteQuickSearch(QuickSearch quickSearch) {
QuickSearchDao dao = db.quickSearchDao();
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
dao.delete(quickSearch);
}
@@ -312,8 +541,9 @@ public class EhDB {
int offset = reverse ? toPosition : fromPosition;
int limit = reverse ? fromPosition - toPosition + 1 : toPosition - fromPosition + 1;
QuickSearchDao dao = db.quickSearchDao();
List<QuickSearch> list = dao.list(offset, limit);
QuickSearchDao dao = sDaoSession.getQuickSearchDao();
List<QuickSearch> list = dao.queryBuilder().orderAsc(QuickSearchDao.Properties.Time)
.offset(offset).limit(limit).list();
int step = reverse ? 1 : -1;
int start = reverse ? limit - 1 : 0;
@@ -324,19 +554,15 @@ public class EhDB {
}
list.get(start).setTime(toTime);
dao.update(list);
dao.updateInTx(list);
}
public static synchronized List<HistoryInfo> getHistoryList() {
return db.historyDao().list();
}
public static synchronized PagingSource<Integer, HistoryInfo> getHistoryLazyList() {
return db.historyDao().listLazy();
public static synchronized LazyList<HistoryInfo> getHistoryLazyList() {
return sDaoSession.getHistoryDao().queryBuilder().orderDesc(HistoryDao.Properties.Time).listLazy();
}
public static synchronized void putHistoryInfo(GalleryInfo galleryInfo) {
HistoryDao dao = db.historyDao();
HistoryDao dao = sDaoSession.getHistoryDao();
HistoryInfo info = dao.load(galleryInfo.gid);
if (null != info) {
// Update time
@@ -347,42 +573,49 @@ public class EhDB {
info = new HistoryInfo(galleryInfo);
info.time = System.currentTimeMillis();
dao.insert(info);
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
.limit(-1).offset(MAX_HISTORY_COUNT).list();
dao.deleteInTx(list);
}
}
public static synchronized void putHistoryInfo(List<HistoryInfo> historyInfoList) {
HistoryDao dao = db.historyDao();
HistoryDao dao = sDaoSession.getHistoryDao();
for (HistoryInfo info : historyInfoList) {
if (null == dao.load(info.gid)) {
dao.insert(info);
}
}
List<HistoryInfo> list = dao.queryBuilder().orderDesc(HistoryDao.Properties.Time)
.limit(-1).offset(MAX_HISTORY_COUNT).list();
dao.deleteInTx(list);
}
public static synchronized void deleteHistoryInfo(HistoryInfo info) {
HistoryDao dao = db.historyDao();
HistoryDao dao = sDaoSession.getHistoryDao();
dao.delete(info);
}
public static synchronized void clearHistoryInfo() {
HistoryDao dao = db.historyDao();
HistoryDao dao = sDaoSession.getHistoryDao();
dao.deleteAll();
}
public static synchronized List<Filter> getAllFilter() {
return db.filterDao().list();
return sDaoSession.getFilterDao().queryBuilder().list();
}
public static synchronized boolean addFilter(Filter filter) {
Filter existFilter;
try {
existFilter = db.filterDao().load(filter.text, filter.mode);
existFilter = sDaoSession.getFilterDao().queryBuilder().where(FilterDao.Properties.Text.eq(filter.text), FilterDao.Properties.Mode.eq(filter.mode)).unique();
} catch (Exception e) {
existFilter = null;
}
if (existFilter == null) {
filter.setId(null);
filter.setId(db.filterDao().insert(filter));
filter.setId(sDaoSession.getFilterDao().insert(filter));
return true;
} else {
return false;
@@ -390,19 +623,23 @@ public class EhDB {
}
public static synchronized void deleteFilter(Filter filter) {
db.filterDao().delete(filter);
sDaoSession.getFilterDao().delete(filter);
}
public static synchronized void triggerFilter(Filter filter) {
filter.setEnable(!filter.enable);
db.filterDao().update(filter);
sDaoSession.getFilterDao().update(filter);
}
private static <T> boolean copyDao(BasicDao<T> from, BasicDao<T> to) {
List<T> list = from.list();
for (T item : list)
to.insert(item);
return false;
private static <T> boolean copyDao(AbstractDao<T, ?> from, AbstractDao<T, ?> to) {
try (CloseableListIterator<T> iterator = from.queryBuilder().listIterator()) {
while (iterator.hasNext()) {
to.insert(iterator.next());
}
} catch (IOException e) {
return false;
}
return true;
}
public static synchronized boolean exportDB(Context context, Uri uri) {
@@ -410,24 +647,31 @@ public class EhDB {
// Delete old export db
context.deleteDatabase(ehExportName);
EhDatabase newDb = Room.databaseBuilder(context, EhDatabase.class, ehExportName).allowMainThreadQueries().build();
DBOpenHelper helper = new DBOpenHelper(context.getApplicationContext(), ehExportName, null);
try {
// Copy data to a export db
if (copyDao(db.downloadsDao(), newDb.downloadsDao()))
return false;
if (copyDao(db.downloadLabelDao(), newDb.downloadLabelDao()))
return false;
if (copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao()))
return false;
if (copyDao(db.historyDao(), newDb.historyDao()))
return false;
if (copyDao(db.quickSearchDao(), newDb.quickSearchDao()))
return false;
if (copyDao(db.localFavoritesDao(), newDb.localFavoritesDao()))
return false;
if (copyDao(db.filterDao(), newDb.filterDao()))
return false;
try (SQLiteDatabase db = helper.getWritableDatabase()) {
DaoMaster daoMaster = new DaoMaster(db);
DaoSession exportSession = daoMaster.newSession();
if (!copyDao(sDaoSession.getDownloadsDao(), exportSession.getDownloadsDao()))
return false;
if (!copyDao(sDaoSession.getDownloadLabelDao(), exportSession.getDownloadLabelDao()))
return false;
if (!copyDao(sDaoSession.getDownloadDirnameDao(), exportSession.getDownloadDirnameDao()))
return false;
if (!copyDao(sDaoSession.getHistoryDao(), exportSession.getHistoryDao()))
return false;
if (!copyDao(sDaoSession.getQuickSearchDao(), exportSession.getQuickSearchDao()))
return false;
if (!copyDao(sDaoSession.getLocalFavoritesDao(), exportSession.getLocalFavoritesDao()))
return false;
if (!copyDao(sDaoSession.getBookmarksDao(), exportSession.getBookmarksDao()))
return false;
if (!copyDao(sDaoSession.getFilterDao(), exportSession.getFilterDao()))
return false;
}
// Copy export db to data dir
File dbFile = context.getDatabasePath(ehExportName);
@@ -474,45 +718,42 @@ public class EhDB {
inputStream.close();
outputStream.close();
SQLiteDatabase oldDB = SQLiteDatabase.openDatabase(
SQLiteDatabase db = SQLiteDatabase.openDatabase(
file.getPath(), null, SQLiteDatabase.NO_LOCALIZED_COLLATORS);
int newVersion = CUR_DB_VER;
int oldVersion = oldDB.getVersion();
int newVersion = DaoMaster.SCHEMA_VERSION;
int oldVersion = db.getVersion();
if (oldVersion < newVersion) {
upgradeDB(oldDB, oldVersion);
oldDB.setVersion(newVersion);
upgradeDB(db, oldVersion);
db.setVersion(newVersion);
} else if (oldVersion > newVersion) {
return context.getString(R.string.cant_read_the_file);
}
String tmpDBName = "tmp.db";
context.deleteDatabase(tmpDBName);
EhDatabase oldRoomDatabase = Room.databaseBuilder(context, EhDatabase.class, tmpDBName)
.createFromFile(file).allowMainThreadQueries().build();
DaoMaster daoMaster = new DaoMaster(db);
DaoSession session = daoMaster.newSession();
// Downloads
DownloadManager manager = EhApplication.getDownloadManager(context);
List<DownloadInfo> downloadInfoList = oldRoomDatabase.downloadsDao().list();
List<DownloadInfo> downloadInfoList = session.getDownloadsDao().queryBuilder().list();
manager.addDownload(downloadInfoList, false);
// Download label
List<DownloadLabel> downloadLabelList = oldRoomDatabase.downloadLabelDao().list();
List<DownloadLabel> downloadLabelList = session.getDownloadLabelDao().queryBuilder().list();
manager.addDownloadLabel(downloadLabelList);
// Download dirname
List<DownloadDirname> downloadDirnameList = oldRoomDatabase.downloadDirnameDao().list();
List<DownloadDirname> downloadDirnameList = session.getDownloadDirnameDao().queryBuilder().list();
for (DownloadDirname dirname : downloadDirnameList) {
putDownloadDirname(dirname.getGid(), dirname.getDirname());
}
// History
List<HistoryInfo> historyInfoList = oldRoomDatabase.historyDao().list();
List<HistoryInfo> historyInfoList = session.getHistoryDao().queryBuilder().list();
putHistoryInfo(historyInfoList);
// QuickSearch
List<QuickSearch> quickSearchList = oldRoomDatabase.quickSearchDao().list();
List<QuickSearch> currentQuickSearchList = db.quickSearchDao().list();
List<QuickSearch> quickSearchList = session.getQuickSearchDao().queryBuilder().list();
List<QuickSearch> currentQuickSearchList = sDaoSession.getQuickSearchDao().queryBuilder().list();
List<QuickSearch> importList = new ArrayList<>();
for (QuickSearch quickSearch : quickSearchList) {
String name = quickSearch.name;
@@ -531,14 +772,17 @@ public class EhDB {
importQuickSearch(importList);
// LocalFavorites
List<LocalFavoriteInfo> localFavoriteInfoList = oldRoomDatabase.localFavoritesDao().list();
List<LocalFavoriteInfo> localFavoriteInfoList = session.getLocalFavoritesDao().queryBuilder().list();
for (LocalFavoriteInfo info : localFavoriteInfoList) {
putLocalFavorites(info);
}
// Bookmarks
// TODO
// Filter
List<Filter> filterList = oldRoomDatabase.filterDao().list();
List<Filter> currentFilterList = db.filterDao().list();
List<Filter> filterList = session.getFilterDao().queryBuilder().list();
List<Filter> currentFilterList = sDaoSession.getFilterDao().queryBuilder().list();
for (Filter filter : filterList) {
if (!currentFilterList.contains(filter)) {
addFilter(filter);
@@ -552,4 +796,46 @@ public class EhDB {
return context.getString(R.string.cant_read_the_file);
}
}
private static class DBOpenHelper extends DaoMaster.OpenHelper {
public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
super(context, name, factory);
}
@Override
public void onCreate(SQLiteDatabase db) {
super.onCreate(db);
sNewDB = true;
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
upgradeDB(db, oldVersion);
}
}
private static class OldDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "data";
private static final int VERSION = 4;
private static final String TABLE_GALLERY = "gallery";
private static final String TABLE_LOCAL_FAVOURITE = "local_favourite";
private static final String TABLE_TAG = "tag";
private static final String TABLE_DOWNLOAD = "download";
private static final String TABLE_HISTORY = "history";
public OldDBHelper(Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
@@ -23,7 +23,6 @@ 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> {
@@ -35,8 +34,7 @@ public class ImageBitmapHelper implements ValueHelper<ImageBitmap> {
public ImageBitmap decode(@NonNull InputStreamPipe isPipe) {
try {
isPipe.obtain();
FileInputStream is = (FileInputStream) isPipe.open();
return ImageBitmap.decode(is.getFD());
return ImageBitmap.decode(isPipe.open());
} catch (OutOfMemoryError | IOException e) {
return null;
} finally {
@@ -76,8 +76,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";
public static final String KEY_MEDIA_SCAN = "media_scan";
@@ -97,13 +95,11 @@ public class Settings {
public static final String KEY_BUILT_IN_HOSTS = "built_in_hosts_2";
public static final String KEY_DOMAIN_FRONTING = "domain_fronting";
public static final String KEY_APP_LANGUAGE = "app_language";
public static final String KEY_LIST_THUMB_SIZE = "list_tile_size";
private static final String TAG = Settings.class.getSimpleName();
private static final String KEY_VERSION_CODE = "version_code";
private static final int DEFAULT_VERSION_CODE = 0;
private static final String KEY_DISPLAY_NAME = "display_name";
private static final String DEFAULT_DISPLAY_NAME = null;
private static final int DEFAULT_LIST_THUMB_SIZE = 40;
private static final String KEY_AVATAR = "avatar";
private static final String DEFAULT_AVATAR = null;
private static final String KEY_SHOW_WARNING = "show_warning";
@@ -276,9 +272,11 @@ public class Settings {
private static final int DEFAULT_DOWNLOAD_DELAY = 0;
private static final String KEY_REQUEST_NEWS = "request_news";
private static final boolean DEFAULT_REQUEST_NEWS = true;
private static final String KEY_ARCHIVE_PASSWDS = "archive_passwds";
public static boolean LIST_THUMB_SIZE_INITED = false;
private static int LIST_THUMB_SIZE = 40;
private static Context sContext;
private static SharedPreferences sSettingsPre;
private static EhConfig sEhConfig;
@@ -1170,22 +1168,4 @@ public class Settings {
public static void putPasswdToArchivePasswds(String value) {
putStringToStringSet(KEY_ARCHIVE_PASSWDS, value);
}
public static int getListThumbSize() {
if (LIST_THUMB_SIZE_INITED) {
return LIST_THUMB_SIZE;
}
int size = 3 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE);
LIST_THUMB_SIZE = size;
LIST_THUMB_SIZE_INITED = true;
return size;
}
public static boolean getNotificationRequired() {
return getBoolean(KEY_NOTIFICATION_REQUIRED, false);
}
public static void putNotificationRequired() {
putBoolean(KEY_NOTIFICATION_REQUIRED, true);
}
}
@@ -19,12 +19,13 @@ package com.hippo.ehviewer.client.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import com.hippo.ehviewer.client.EhUrl;
import com.hippo.network.UrlBuilder;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FavListUrlBuilder implements Parcelable {
@@ -92,13 +93,17 @@ public class FavListUrlBuilder implements Parcelable {
ub.addQuery("favcat", "all");
}
if (!TextUtils.isEmpty(mKeyword)) {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
// Name
ub.addQuery("sn", "on");
// Tags
ub.addQuery("st", "on");
// Note
ub.addQuery("sf", "on");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Can't URLEncoder.encode " + mKeyword);
}
}
if (mIndex > 0) {
ub.addQuery("page", Integer.toString(mIndex));
@@ -20,9 +20,6 @@ import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import java.util.regex.Pattern;
@@ -111,52 +108,30 @@ public class GalleryInfo implements Parcelable {
return new GalleryInfo[size];
}
};
@PrimaryKey
@ColumnInfo(name = "GID")
public long gid;
@ColumnInfo(name = "TOKEN")
public String token;
@ColumnInfo(name = "TITLE")
public String title;
@ColumnInfo(name = "TITLE_JPN")
public String titleJpn;
@ColumnInfo(name = "THUMB")
public String thumb;
@ColumnInfo(name = "CATEGORY")
public int category;
@ColumnInfo(name = "POSTED")
public String posted;
@ColumnInfo(name = "UPLOADER")
public String uploader;
@Ignore
public boolean disowned;
@ColumnInfo(name = "RATING")
public float rating;
@Ignore
public boolean rated;
@Nullable
@Ignore
public String[] simpleTags;
@Ignore
public int pages;
@Ignore
public int thumbWidth;
@Ignore
public int thumbHeight;
@Ignore
public int spanSize;
@Ignore
public int spanIndex;
@Ignore
public int spanGroupIndex;
/**
* language from title
*/
@ColumnInfo(name = "SIMPLE_LANGUAGE")
public String simpleLanguage;
@Ignore
public int favoriteSlot = -2;
@Ignore
public String favoriteName;
public GalleryInfo() {
@@ -195,7 +170,6 @@ public class GalleryInfo implements Parcelable {
}
private void generateSLangFromTags() {
assert simpleTags != null;
for (String tag : simpleTags) {
for (int i = 0; i < S_LANGS.length; i++) {
if (S_LANG_TAGS[i].equals(tag)) {
@@ -32,11 +32,11 @@ import com.hippo.network.UrlBuilder;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.StringUtils;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class ListUrlBuilder implements Cloneable, Parcelable {
@@ -383,8 +383,8 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
break;
case "f_search":
try {
keyword = URLDecoder.decode(value, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
keyword = URLDecoder.decode(value, "utf-8");
} catch (UnsupportedEncodingException | IllegalArgumentException e) {
// Ignore
}
break;
@@ -514,7 +514,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
if (mKeyword != null) {
String keyword = mKeyword.trim();
if (!keyword.isEmpty()) {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
try {
ub.addQuery("f_search", URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
}
}
if (mSHash != null) {
@@ -561,7 +565,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
case MODE_UPLOADER: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("uploader/");
sb.append(URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
@@ -570,7 +578,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
case MODE_TAG: {
StringBuilder sb = new StringBuilder(EhUrl.getHost());
sb.append("tag/");
sb.append(URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append('/').append(mPageIndex);
}
@@ -583,7 +595,11 @@ public class ListUrlBuilder implements Cloneable, Parcelable {
case MODE_TOPLIST:
StringBuilder sb = new StringBuilder(EhUrl.HOST_E);
sb.append("toplist.php?tl=");
sb.append(URLEncoder.encode(mKeyword, StandardCharsets.UTF_8));
try {
sb.append(URLEncoder.encode(mKeyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Empty
}
if (mPageIndex != 0) {
sb.append("&p=").append(mPageIndex);
}
@@ -23,10 +23,10 @@ import com.hippo.ehviewer.client.EhUrl;
import com.hippo.ehviewer.client.data.ListUrlBuilder;
import com.hippo.yorozuya.Utilities;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
public final class GalleryListUrlParser {
@@ -91,7 +91,11 @@ public final class GalleryListUrlParser {
uploader = path.substring(prefixLength, index);
}
uploader = URLDecoder.decode(uploader, StandardCharsets.UTF_8);
try {
uploader = URLDecoder.decode(uploader, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
}
if (TextUtils.isEmpty(uploader)) {
return null;
@@ -116,7 +120,11 @@ public final class GalleryListUrlParser {
tag = path.substring(prefixLength, index);
}
tag = URLDecoder.decode(tag, StandardCharsets.UTF_8);
try {
tag = URLDecoder.decode(tag, "utf-8");
} catch (UnsupportedEncodingException e) {
return null;
}
if (TextUtils.isEmpty(tag)) {
return null;
@@ -1,9 +0,0 @@
package com.hippo.ehviewer.dao;
import java.util.List;
public interface BasicDao<T> {
List<T> list();
long insert(T t);
}
@@ -1,160 +0,0 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "BOOKMARKS")
public class BookmarkInfo extends GalleryInfo {
public static final Creator<BookmarkInfo> CREATOR = new Creator<>() {
@Override
public BookmarkInfo createFromParcel(Parcel source) {
return new BookmarkInfo(source);
}
@Override
public BookmarkInfo[] newArray(int size) {
return new BookmarkInfo[size];
}
};
@ColumnInfo(name = "PAGE")
public int page;
@ColumnInfo(name = "TIME")
public long time;
protected BookmarkInfo(Parcel in) {
super(in);
this.page = in.readInt();
this.time = in.readLong();
}
public BookmarkInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public BookmarkInfo(int page, long time) {
this.page = page;
this.time = time;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.page);
dest.writeLong(this.time);
}
}
@@ -1,20 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface BookmarksDao extends BasicDao<BookmarkInfo> {
@Insert
long insert(BookmarkInfo bookmark);
@Delete
void delete(BookmarkInfo bookmark);
@Query("SELECT * FROM BOOKMARKS ORDER BY TIME DESC")
List<BookmarkInfo> list();
}
@@ -1,30 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "DOWNLOAD_DIRNAME")
public class DownloadDirname {
@PrimaryKey
@ColumnInfo(name = "GID")
private long gid;
@ColumnInfo(name = "DIRNAME")
private String dirname;
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getDirname() {
return dirname;
}
public void setDirname(String dirname) {
this.dirname = dirname;
}
}
@@ -1,29 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadDirnameDao extends BasicDao<DownloadDirname> {
@Query("SELECT * FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
DownloadDirname load(long gid);
@Update
void update(DownloadDirname downloadDirname);
@Insert
long insert(DownloadDirname downloadDirname);
@Query("DELETE FROM DOWNLOAD_DIRNAME WHERE GID = :gid")
void deleteByKey(long gid);
@Query("DELETE FROM DOWNLOAD_DIRNAME")
void deleteAll();
@Query("SELECT * FROM DOWNLOAD_DIRNAME")
List<DownloadDirname> list();
}
@@ -1,252 +0,0 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "DOWNLOADS")
public class DownloadInfo extends GalleryInfo {
public static final Creator<DownloadInfo> CREATOR = new Creator<>() {
@Override
public DownloadInfo createFromParcel(Parcel source) {
return new DownloadInfo(source);
}
@Override
public DownloadInfo[] newArray(int size) {
return new DownloadInfo[size];
}
};
public static final int STATE_INVALID = -1;
public static final int STATE_NONE = 0;
public static final int STATE_WAIT = 1;
public static final int STATE_DOWNLOAD = 2;
public static final int STATE_FINISH = 3;
public static final int STATE_FAILED = 4;
@ColumnInfo(name = "STATE")
public int state;
@ColumnInfo(name = "LEGACY")
public int legacy;
@ColumnInfo(name = "TIME")
public long time;
@ColumnInfo(name = "LABEL")
public String label;
@Ignore
public long speed;
@Ignore
public long remaining;
@Ignore
public int finished;
@Ignore
public int downloaded;
@Ignore
public int total;
DownloadInfo() {
}
protected DownloadInfo(Parcel in) {
super(in);
this.state = in.readInt();
this.legacy = in.readInt();
this.time = in.readLong();
this.label = in.readString();
}
public DownloadInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public DownloadInfo(int state, int legacy, long time, String label, long speed, long remaining,
int finished, int downloaded, int total) {
this.state = state;
this.legacy = legacy;
this.time = time;
this.label = label;
this.speed = speed;
this.remaining = remaining;
this.finished = finished;
this.downloaded = downloaded;
this.total = total;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public int getLegacy() {
return legacy;
}
public void setLegacy(int legacy) {
this.legacy = legacy;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.state);
dest.writeInt(this.legacy);
dest.writeLong(this.time);
dest.writeString(this.label);
}
public long getSpeed() {
return this.speed;
}
public void setSpeed(long speed) {
this.speed = speed;
}
public long getRemaining() {
return this.remaining;
}
public void setRemaining(long remaining) {
this.remaining = remaining;
}
public int getFinished() {
return this.finished;
}
public void setFinished(int finished) {
this.finished = finished;
}
public int getDownloaded() {
return this.downloaded;
}
public void setDownloaded(int downloaded) {
this.downloaded = downloaded;
}
public int getTotal() {
return this.total;
}
public void setTotal(int total) {
this.total = total;
}
}
@@ -1,40 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "DOWNLOAD_LABELS")
public class DownloadLabel {
@PrimaryKey
@ColumnInfo(name = "_id")
private Long id;
@ColumnInfo(name = "LABEL")
private String label;
@ColumnInfo(name = "TIME")
private long time;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
}
@@ -1,30 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadLabelDao extends BasicDao<DownloadLabel> {
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC")
List<DownloadLabel> list();
@Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC LIMIT :limit OFFSET :offset")
List<DownloadLabel> list(int offset, int limit);
@Update
void update(List<DownloadLabel> downloadLabels);
@Update
void update(DownloadLabel downloadLabel);
@Insert
long insert(DownloadLabel downloadLabel);
@Delete
void delete(DownloadLabel downloadLabel);
}
@@ -1,27 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface DownloadsDao extends BasicDao<DownloadInfo> {
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME DESC")
List<DownloadInfo> list();
@Query("SELECT * FROM DOWNLOADS WHERE GID = :gid")
DownloadInfo load(long gid);
@Update
void update(DownloadInfo downloadInfo);
@Insert
long insert(DownloadInfo downloadInfo);
@Delete
void delete(DownloadInfo downloadInfo);
}
@@ -1,23 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {BookmarkInfo.class, DownloadInfo.class, DownloadLabel.class, DownloadDirname.class, Filter.class, HistoryInfo.class, LocalFavoriteInfo.class, QuickSearch.class}, version = 4, exportSchema = false)
public abstract class EhDatabase extends RoomDatabase {
public abstract BookmarksDao bookmarksBao();
public abstract DownloadDirnameDao downloadDirnameDao();
public abstract DownloadLabelDao downloadLabelDao();
public abstract DownloadsDao downloadsDao();
public abstract FilterDao filterDao();
public abstract HistoryDao historyDao();
public abstract LocalFavoritesDao localFavoritesDao();
public abstract QuickSearchDao quickSearchDao();
}
@@ -1,67 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import com.hippo.util.HashCodeUtils;
import com.hippo.yorozuya.ObjectUtils;
@Entity(tableName = "FILTER")
public class Filter {
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "TEXT")
public String text;
@ColumnInfo(name = "ENABLE")
public Boolean enable;
@PrimaryKey
@ColumnInfo(name = "_id")
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Boolean getEnable() {
return enable;
}
public void setEnable(Boolean enable) {
this.enable = enable;
}
@Override
public int hashCode() {
return HashCodeUtils.hashCode(mode, text);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Filter)) {
return false;
}
Filter filter = (Filter) o;
return filter.mode == mode && ObjectUtils.equal(filter.text, text);
}
}
@@ -1,27 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface FilterDao extends BasicDao<Filter> {
@Query("SELECT * FROM FILTER")
List<Filter> list();
@Update
void update(Filter filter);
@Insert
long insert(Filter filter);
@Delete
void delete(Filter filter);
@Query("SELECT * FROM FILTER WHERE TEXT = :text AND MODE = :mode")
Filter load(String text, int mode);
}
@@ -1,40 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.paging.PagingSource;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface HistoryDao extends BasicDao<HistoryInfo> {
@Query("SELECT * FROM HISTORY WHERE GID = :gid")
HistoryInfo load(long gid);
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
List<HistoryInfo> list();
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC LIMIT :limit OFFSET :offset")
List<HistoryInfo> list(int offset, int limit);
@Query("SELECT * FROM HISTORY ORDER BY TIME DESC")
PagingSource<Integer, HistoryInfo> listLazy();
@Update
void update(HistoryInfo historyInfo);
@Insert
long insert(HistoryInfo historyInfo);
@Delete
void delete(HistoryInfo historyInfos);
@Delete
void delete(List<HistoryInfo> historyInfos);
@Query("DELETE FROM HISTORY")
void deleteAll();
}
@@ -1,160 +0,0 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "HISTORY")
public class HistoryInfo extends GalleryInfo {
public static final Creator<HistoryInfo> CREATOR = new Creator<>() {
@Override
public HistoryInfo createFromParcel(Parcel source) {
return new HistoryInfo(source);
}
@Override
public HistoryInfo[] newArray(int size) {
return new HistoryInfo[size];
}
};
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "TIME")
public long time;
protected HistoryInfo(Parcel in) {
super(in);
this.mode = in.readInt();
this.time = in.readLong();
}
public HistoryInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public HistoryInfo(int mode, long time) {
this.mode = mode;
this.time = time;
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(this.mode);
dest.writeLong(this.time);
}
}
@@ -1,147 +0,0 @@
package com.hippo.ehviewer.dao;
import android.os.Parcel;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import com.hippo.ehviewer.client.data.GalleryInfo;
@Entity(tableName = "LOCAL_FAVORITES")
public class LocalFavoriteInfo extends GalleryInfo {
public static final Creator<LocalFavoriteInfo> CREATOR = new Creator<>() {
@Override
public LocalFavoriteInfo createFromParcel(Parcel source) {
return new LocalFavoriteInfo(source);
}
@Override
public LocalFavoriteInfo[] newArray(int size) {
return new LocalFavoriteInfo[size];
}
};
@ColumnInfo(name = "TIME")
public long time;
protected LocalFavoriteInfo(Parcel in) {
super(in);
this.time = in.readLong();
}
public LocalFavoriteInfo(GalleryInfo galleryInfo) {
this.gid = galleryInfo.gid;
this.token = galleryInfo.token;
this.title = galleryInfo.title;
this.titleJpn = galleryInfo.titleJpn;
this.thumb = galleryInfo.thumb;
this.category = galleryInfo.category;
this.posted = galleryInfo.posted;
this.uploader = galleryInfo.uploader;
this.rating = galleryInfo.rating;
this.simpleTags = galleryInfo.simpleTags;
this.simpleLanguage = galleryInfo.simpleLanguage;
}
public LocalFavoriteInfo() {
}
public long getGid() {
return gid;
}
public void setGid(long gid) {
this.gid = gid;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitleJpn() {
return titleJpn;
}
public void setTitleJpn(String titleJpn) {
this.titleJpn = titleJpn;
}
public String getThumb() {
return thumb;
}
public void setThumb(String thumb) {
this.thumb = thumb;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getPosted() {
return posted;
}
public void setPosted(String posted) {
this.posted = posted;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public float getRating() {
return rating;
}
public void setRating(float rating) {
this.rating = rating;
}
public String getSimpleLanguage() {
return simpleLanguage;
}
public void setSimpleLanguage(String simpleLanguage) {
this.simpleLanguage = simpleLanguage;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(this.time);
}
}
@@ -1,29 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface LocalFavoritesDao extends BasicDao<LocalFavoriteInfo> {
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC")
List<LocalFavoriteInfo> list();
@Query("SELECT * FROM LOCAL_FAVORITES WHERE TITLE LIKE :title ORDER BY TIME DESC")
List<LocalFavoriteInfo> list(String title);
@Query("SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid")
LocalFavoriteInfo load(long gid);
@Insert
long insert(LocalFavoriteInfo localFavoriteInfo);
@Delete
void delete(LocalFavoriteInfo localFavoriteInfo);
@Query("DELETE FROM LOCAL_FAVORITES WHERE GID = :gid")
void deleteByKey(long gid);
}
@@ -1,117 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.annotation.NonNull;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "QUICK_SEARCH")
public class QuickSearch {
@PrimaryKey
@ColumnInfo(name = "_id")
public Long id;
@ColumnInfo(name = "NAME")
public String name;
@ColumnInfo(name = "MODE")
public int mode;
@ColumnInfo(name = "CATEGORY")
public int category;
@ColumnInfo(name = "KEYWORD")
public String keyword;
@ColumnInfo(name = "ADVANCE_SEARCH")
public int advanceSearch;
@ColumnInfo(name = "MIN_RATING")
public int minRating;
@ColumnInfo(name = "PAGE_FROM")
public int pageFrom;
@ColumnInfo(name = "PAGE_TO")
public int pageTo;
@ColumnInfo(name = "TIME")
public long time;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public int getAdvanceSearch() {
return advanceSearch;
}
public void setAdvanceSearch(int advanceSearch) {
this.advanceSearch = advanceSearch;
}
public int getMinRating() {
return minRating;
}
public void setMinRating(int minRating) {
this.minRating = minRating;
}
public int getPageFrom() {
return pageFrom;
}
public void setPageFrom(int pageFrom) {
this.pageFrom = pageFrom;
}
public int getPageTo() {
return pageTo;
}
public void setPageTo(int pageTo) {
this.pageTo = pageTo;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
@NonNull
@Override
public String toString() {
return name;
}
}
@@ -1,30 +0,0 @@
package com.hippo.ehviewer.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface QuickSearchDao extends BasicDao<QuickSearch> {
@Query("SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC")
List<QuickSearch> list();
@Query("SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC LIMIT :limit OFFSET :offset")
List<QuickSearch> list(int offset, int limit);
@Update
void update(List<QuickSearch> downloadLabels);
@Update
void update(QuickSearch quickSearch);
@Insert
long insert(QuickSearch quickSearch);
@Delete
void delete(QuickSearch quickSearch);
}
@@ -516,7 +516,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
DownloadInfo info = mAllInfoMap.get(gid);
if (info != null) {
// Remove from DB
EhDB.removeDownloadInfo(info);
EhDB.removeDownloadInfo(info.gid);
// Remove all list and map
mAllInfoList.remove(info);
@@ -552,7 +552,7 @@ public class DownloadManager implements SpiderQueen.OnSpiderListener {
}
// Remove from DB
EhDB.removeDownloadInfo(info);
EhDB.removeDownloadInfo(info.gid);
// Remove from all info map
mAllInfoList.remove(info);
@@ -0,0 +1,581 @@
/*
* 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.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.scene.DownloadsScene;
import com.hippo.scene.StageActivity;
import com.hippo.util.ReadableTime;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.collect.LongList;
import com.hippo.yorozuya.collect.SparseJBArray;
import com.hippo.yorozuya.collect.SparseJLArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class DownloadService extends Service implements DownloadManager.DownloadListener {
public static final String ACTION_START = "start";
public static final String ACTION_START_RANGE = "start_range";
public static final String ACTION_START_ALL = "start_all";
public static final String ACTION_STOP = "stop";
public static final String ACTION_STOP_RANGE = "stop_range";
public static final String ACTION_STOP_CURRENT = "stop_current";
public static final String ACTION_STOP_ALL = "stop_all";
public static final String ACTION_DELETE = "delete";
public static final String ACTION_DELETE_RANGE = "delete_range";
public static final String ACTION_CLEAR = "clear";
public static final String KEY_GALLERY_INFO = "gallery_info";
public static final String KEY_LABEL = "label";
public static final String KEY_GID = "gid";
public static final String KEY_GID_LIST = "gid_list";
private static final int ID_DOWNLOADING = 1;
private static final int ID_DOWNLOADED = 2;
private static final int ID_509 = 3;
private final static SparseJBArray sItemStateArray = new SparseJBArray();
private final static SparseJLArray<String> sItemTitleArray = new SparseJLArray<>();
private static int sFailedCount;
private static int sFinishedCount;
private static int sDownloadedCount;
@Nullable
private NotificationManagerCompat mNotifyManager;
@Nullable
private DownloadManager mDownloadManager;
private NotificationCompat.Builder mDownloadingBuilder;
private NotificationCompat.Builder mDownloadedBuilder;
private NotificationCompat.Builder m509dBuilder;
private NotificationDelay mDownloadingDelay;
private NotificationDelay mDownloadedDelay;
private NotificationDelay m509Delay;
private String CHANNEL_ID;
public static void clear() {
sFailedCount = 0;
sFinishedCount = 0;
sDownloadedCount = 0;
sItemStateArray.clear();
sItemTitleArray.clear();
}
@Override
public void onCreate() {
super.onCreate();
CHANNEL_ID = getPackageName() + ".download";
mNotifyManager = NotificationManagerCompat.from(this);
mNotifyManager.createNotificationChannel(
new NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.download_service))
.build());
mDownloadManager = EhApplication.getDownloadManager(getApplicationContext());
mDownloadManager.setDownloadListener(this);
ensureDownloadingBuilder();
mDownloadingBuilder.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true);
startForeground(ID_DOWNLOADING, mDownloadingBuilder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
mNotifyManager = null;
if (mDownloadManager != null) {
mDownloadManager.setDownloadListener(null);
mDownloadManager = null;
}
mDownloadingBuilder = null;
mDownloadedBuilder = null;
m509dBuilder = null;
if (mDownloadingDelay != null) {
mDownloadingDelay.release();
}
if (mDownloadedDelay != null) {
mDownloadedDelay.release();
}
if (m509Delay != null) {
m509Delay.release();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleIntent(intent);
return START_STICKY;
}
private void handleIntent(Intent intent) {
String action = null;
if (intent != null) {
action = intent.getAction();
}
if (ACTION_START.equals(action)) {
GalleryInfo gi = intent.getParcelableExtra(KEY_GALLERY_INFO);
String label = intent.getStringExtra(KEY_LABEL);
if (gi != null && mDownloadManager != null) {
mDownloadManager.startDownload(gi, label);
}
} else if (ACTION_START_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.startRangeDownload(gidList);
}
} else if (ACTION_START_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.startAllDownload();
}
} else if (ACTION_STOP.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.stopDownload(gid);
}
} else if (ACTION_STOP_CURRENT.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopCurrentDownload();
}
} else if (ACTION_STOP_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.stopRangeDownload(gidList);
}
} else if (ACTION_STOP_ALL.equals(action)) {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
} else if (ACTION_DELETE.equals(action)) {
long gid = intent.getLongExtra(KEY_GID, -1);
if (gid != -1 && mDownloadManager != null) {
mDownloadManager.deleteDownload(gid);
}
} else if (ACTION_DELETE_RANGE.equals(action)) {
LongList gidList = intent.getParcelableExtra(KEY_GID_LIST);
if (gidList != null && mDownloadManager != null) {
mDownloadManager.deleteRangeDownload(gidList);
}
} else if (ACTION_CLEAR.equals(action)) {
clear();
}
checkStopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new IllegalStateException("No bindService");
}
private void ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return;
}
Intent stopAllIntent = new Intent(this, DownloadService.class);
stopAllIntent.setAction(ACTION_STOP_ALL);
PendingIntent piStopAll = PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
.setShowWhen(false)
.setChannelId(CHANNEL_ID);
mDownloadingDelay = new NotificationDelay(this, mNotifyManager, mDownloadingBuilder, ID_DOWNLOADING);
}
private void ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return;
}
Intent clearIntent = new Intent(this, DownloadService.class);
clearIntent.setAction(ACTION_CLEAR);
PendingIntent piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE);
Bundle bundle = new Bundle();
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity);
mDownloadedDelay = new NotificationDelay(this, mNotifyManager, mDownloadedBuilder, ID_DOWNLOADED);
}
private void ensure509Builder() {
if (m509dBuilder != null) {
return;
}
m509dBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)))
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR);
m509Delay = new NotificationDelay(this, mNotifyManager, m509dBuilder, ID_509);
}
@Override
public void onGet509() {
if (mDownloadManager != null) {
mDownloadManager.stopAllDownload();
}
if (mNotifyManager == null) {
return;
}
ensure509Builder();
m509dBuilder.setWhen(System.currentTimeMillis());
m509Delay.show();
}
@Override
public void onStart(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
Bundle bundle = new Bundle();
bundle.putLong(DownloadsScene.KEY_GID, info.gid);
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.setAction(StageActivity.ACTION_START_SCENE);
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene.class.getName());
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle);
PendingIntent piActivity = PendingIntent.getActivity(DownloadService.this, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity);
mDownloadingDelay.startForeground();
}
private void onUpdate(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
ensureDownloadingBuilder();
long speed = info.speed;
if (speed < 0) {
speed = 0;
}
String text = FileUtils.humanReadableByteCount(speed, false) + "/s";
long remaining = info.remaining;
if (remaining >= 0) {
text = getString(R.string.download_speed_text_2, text, ReadableTime.getShortTimeInterval(remaining));
} else {
text = getString(R.string.download_speed_text, text);
}
mDownloadingBuilder.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
.setSubText(info.total == -1 || info.finished == -1 ? null : info.finished + "/" + info.total)
.setProgress(info.total, info.finished, false);
mDownloadingDelay.startForeground();
}
@Override
public void onDownload(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onGetPage(DownloadInfo info) {
onUpdate(info);
}
@Override
public void onFinish(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
ensureDownloadedBuilder();
boolean finish = info.state == DownloadInfo.STATE_FINISH;
long gid = info.gid;
int index = sItemStateArray.indexOfKey(gid);
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
sDownloadedCount++;
if (finish) {
sFinishedCount++;
} else {
sFailedCount++;
}
} else { // Contain
boolean oldFinish = sItemStateArray.valueAt(index);
sItemStateArray.put(gid, finish);
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info));
if (oldFinish && !finish) {
sFinishedCount--;
sFailedCount++;
} else if (!oldFinish && finish) {
sFinishedCount++;
sFailedCount--;
}
}
String text;
boolean needStyle;
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_succeeded, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount);
needStyle = true;
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
if (sItemTitleArray.size() >= 1) {
text = getString(R.string.stat_download_done_line_failed, sItemTitleArray.valueAt(0));
} else {
Log.d("TAG", "WTF, sItemTitleArray is null");
text = getString(R.string.error_unknown);
}
needStyle = false;
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount);
needStyle = true;
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount);
needStyle = true;
}
NotificationCompat.InboxStyle style;
if (needStyle) {
style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(getString(R.string.stat_download_done_title));
SparseJBArray stateArray = sItemStateArray;
for (int i = 0, n = stateArray.size(); i < n; i++) {
long id = stateArray.keyAt(i);
boolean fin = stateArray.valueAt(i);
String title = sItemTitleArray.get(id);
if (title == null) {
continue;
}
style.addLine(getString(fin ? R.string.stat_download_done_line_succeeded :
R.string.stat_download_done_line_failed, title));
}
} else {
style = null;
}
mDownloadedBuilder.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount);
mDownloadedDelay.show();
checkStopSelf();
}
@Override
public void onCancel(DownloadInfo info) {
if (mNotifyManager == null) {
return;
}
if (null != mDownloadingDelay) {
mDownloadingDelay.cancel();
}
checkStopSelf();
}
private void checkStopSelf() {
if (mDownloadManager == null || mDownloadManager.isIdle()) {
stopForeground(true);
stopSelf();
}
}
// TODO Include all notification in one delay
// Avoid frequent notification
private static class NotificationDelay implements Runnable {
private static final int OPS_NOTIFY = 0;
private static final int OPS_CANCEL = 1;
private static final int OPS_START_FOREGROUND = 2;
private static final long DELAY = 1000; // 1s
private final NotificationManagerCompat mNotifyManager;
private final NotificationCompat.Builder mBuilder;
private final int mId;
private Service mService;
private long mLastTime;
private boolean mPosted;
// false for show, true for cancel
@Ops
private int mOps;
public NotificationDelay(Service service, NotificationManagerCompat notifyManager,
NotificationCompat.Builder builder, int id) {
mService = service;
mNotifyManager = notifyManager;
mBuilder = builder;
mId = id;
}
public void release() {
mService = null;
}
public void show() {
if (mPosted) {
mOps = OPS_NOTIFY;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.notify(mId, mBuilder.build());
} else {
// Too quick, post delay
mOps = OPS_NOTIFY;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
mLastTime = now;
}
}
public void cancel() {
if (mPosted) {
mOps = OPS_CANCEL;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId);
} else {
// Too quick, post delay
mOps = OPS_CANCEL;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
public void startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND;
} else {
long now = SystemClock.currentThreadTimeMillis();
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND;
mPosted = true;
SimpleHandler.getInstance().postDelayed(this, DELAY);
}
}
}
@Override
public void run() {
mPosted = false;
switch (mOps) {
case OPS_NOTIFY:
mNotifyManager.notify(mId, mBuilder.build());
break;
case OPS_CANCEL:
mNotifyManager.cancel(mId);
break;
case OPS_START_FOREGROUND:
if (mService != null) {
mService.startForeground(mId, mBuilder.build());
}
break;
}
}
@IntDef({OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND})
@Retention(RetentionPolicy.SOURCE)
private @interface Ops {
}
}
}
@@ -1,550 +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.download
import android.Manifest
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.annotation.IntDef
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.ui.scene.DownloadsScene
import com.hippo.scene.StageActivity
import com.hippo.util.ReadableTime
import com.hippo.yorozuya.FileUtils
import com.hippo.yorozuya.SimpleHandler
import com.hippo.yorozuya.collect.LongList
import com.hippo.yorozuya.collect.SparseJBArray
import com.hippo.yorozuya.collect.SparseJLArray
class DownloadService : Service(), DownloadManager.DownloadListener {
private var mNotifyManager: NotificationManagerCompat? = null
private var mDownloadManager: DownloadManager? = null
private var mDownloadingBuilder: NotificationCompat.Builder? = null
private var mDownloadedBuilder: NotificationCompat.Builder? = null
private var m509dBuilder: NotificationCompat.Builder? = null
private var mDownloadingDelay: NotificationDelay? = null
private var mDownloadedDelay: NotificationDelay? = null
private var m509Delay: NotificationDelay? = null
private var mChannelID: String? = null
override fun onCreate() {
super.onCreate()
mChannelID = "$packageName.download"
mNotifyManager = NotificationManagerCompat.from(this)
mNotifyManager!!.createNotificationChannel(
NotificationChannelCompat.Builder(
mChannelID!!,
NotificationManagerCompat.IMPORTANCE_LOW
)
.setName(getString(R.string.download_service))
.build()
)
mDownloadManager = EhApplication.getDownloadManager(applicationContext)
mDownloadManager!!.setDownloadListener(this)
ensureDownloadingBuilder()
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())
}
override fun onDestroy() {
super.onDestroy()
mNotifyManager = null
if (mDownloadManager != null) {
mDownloadManager!!.setDownloadListener(null)
mDownloadManager = null
}
mDownloadingBuilder = null
mDownloadedBuilder = null
m509dBuilder = null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
handleIntent(intent)
return START_STICKY
}
private fun handleIntent(intent: Intent?) {
var action: String? = null
if (intent != null) {
action = intent.action
}
if (ACTION_START == action) {
val gi = intent!!.getParcelableExtra<GalleryInfo>(KEY_GALLERY_INFO)
val label = intent.getStringExtra(KEY_LABEL)
if (gi != null && mDownloadManager != null) {
mDownloadManager!!.startDownload(gi, label)
}
} else if (ACTION_START_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.startRangeDownload(gidList)
}
} else if (ACTION_START_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.startAllDownload()
}
} else if (ACTION_STOP == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.stopDownload(gid)
}
} else if (ACTION_STOP_CURRENT == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopCurrentDownload()
}
} else if (ACTION_STOP_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.stopRangeDownload(gidList)
}
} else if (ACTION_STOP_ALL == action) {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
} else if (ACTION_DELETE == action) {
val gid = intent!!.getLongExtra(KEY_GID, -1)
if (gid != -1L && mDownloadManager != null) {
mDownloadManager!!.deleteDownload(gid)
}
} else if (ACTION_DELETE_RANGE == action) {
val gidList = intent!!.getParcelableExtra<LongList>(KEY_GID_LIST)
if (gidList != null && mDownloadManager != null) {
mDownloadManager!!.deleteRangeDownload(gidList)
}
} else if (ACTION_CLEAR == action) {
clear()
}
checkStopSelf()
}
override fun onBind(intent: Intent): IBinder? {
throw IllegalStateException("No bindService")
}
private fun ensureDownloadingBuilder() {
if (mDownloadingBuilder != null) {
return
}
val stopAllIntent = Intent(this, DownloadService::class.java)
stopAllIntent.action = ACTION_STOP_ALL
val piStopAll =
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.setAutoCancel(false)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
R.drawable.v_pause_x24,
getString(R.string.stat_download_action_stop_all),
piStopAll
)
.setShowWhen(false)
.setChannelId(mChannelID!!)
mDownloadingDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)
}
private fun ensureDownloadedBuilder() {
if (mDownloadedBuilder != null) {
return
}
val clearIntent = Intent(this, DownloadService::class.java)
clearIntent.action = ACTION_CLEAR
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
val bundle = Bundle()
bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle(getString(R.string.stat_download_done_title))
.setDeleteIntent(piClear)
.setOngoing(false)
.setAutoCancel(true)
.setContentIntent(piActivity)
mDownloadedDelay =
NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)
}
private fun ensure509Builder() {
if (m509dBuilder != null) {
return
}
m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
.setSmallIcon(R.drawable.ic_baseline_warning_24)
.setContentText(getString(R.string.stat_509_alert_title))
.setContentText(getString(R.string.stat_509_alert_text))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text))
)
.setAutoCancel(true)
.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_ERROR)
m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)
}
override fun onGet509() {
if (mDownloadManager != null) {
mDownloadManager!!.stopAllDownload()
}
if (mNotifyManager == null) {
return
}
ensure509Builder()
m509dBuilder!!.setWhen(System.currentTimeMillis())
m509Delay!!.show()
}
override fun onStart(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
val bundle = Bundle()
bundle.putLong(DownloadsScene.KEY_GID, info.gid)
val activityIntent = Intent(this, MainActivity::class.java)
activityIntent.action = StageActivity.ACTION_START_SCENE
activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)
activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)
val piActivity = PendingIntent.getActivity(
this@DownloadService, 0,
activityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(null)
.setSubText(null)
.setProgress(0, 0, true)
.setContentIntent(piActivity)
mDownloadingDelay!!.startForeground()
}
private fun onUpdate(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
ensureDownloadingBuilder()
var speed = info.speed
if (speed < 0) {
speed = 0
}
var text = FileUtils.humanReadableByteCount(speed, false) + "/s"
val remaining = info.remaining
text = if (remaining >= 0) {
getString(
R.string.download_speed_text_2,
text,
ReadableTime.getShortTimeInterval(remaining)
)
} else {
getString(R.string.download_speed_text, text)
}
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
.setProgress(info.total, info.finished, false)
mDownloadingDelay!!.startForeground()
}
override fun onDownload(info: DownloadInfo) {
onUpdate(info)
}
override fun onGetPage(info: DownloadInfo) {
onUpdate(info)
}
override fun onFinish(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
ensureDownloadedBuilder()
val finish = info.state == DownloadInfo.STATE_FINISH
val gid = info.gid
val index = sItemStateArray.indexOfKey(gid)
if (index < 0) { // Not contain
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
sDownloadedCount++
if (finish) {
sFinishedCount++
} else {
sFailedCount++
}
} else { // Contain
val oldFinish = sItemStateArray.valueAt(index)
sItemStateArray.put(gid, finish)
sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
if (oldFinish && !finish) {
sFinishedCount--
sFailedCount++
} else if (!oldFinish && finish) {
sFinishedCount++
sFailedCount--
}
}
val text: String
val needStyle: Boolean
if (sFinishedCount != 0 && sFailedCount == 0) {
if (sFinishedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_succeeded,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount)
needStyle = true
}
} else if (sFinishedCount == 0 && sFailedCount != 0) {
if (sFailedCount == 1) {
text = if (sItemTitleArray.size() >= 1) {
getString(
R.string.stat_download_done_line_failed,
sItemTitleArray.valueAt(0)
)
} else {
Log.d("TAG", "WTF, sItemTitleArray is null")
getString(R.string.error_unknown)
}
needStyle = false
} else {
text = getString(R.string.stat_download_done_text_failed, sFailedCount)
needStyle = true
}
} else {
text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount)
needStyle = true
}
val style: NotificationCompat.InboxStyle?
if (needStyle) {
style = NotificationCompat.InboxStyle()
style.setBigContentTitle(getString(R.string.stat_download_done_title))
val stateArray = sItemStateArray
var i = 0
val n = stateArray.size()
while (i < n) {
val id = stateArray.keyAt(i)
val fin = stateArray.valueAt(i)
val title = sItemTitleArray[id]
if (title == null) {
i++
continue
}
style.addLine(
getString(
if (fin) R.string.stat_download_done_line_succeeded else R.string.stat_download_done_line_failed,
title
)
)
i++
}
} else {
style = null
}
mDownloadedBuilder!!.setContentText(text)
.setStyle(style)
.setWhen(System.currentTimeMillis())
.setNumber(sDownloadedCount)
mDownloadedDelay!!.show()
checkStopSelf()
}
override fun onCancel(info: DownloadInfo) {
if (mNotifyManager == null) {
return
}
if (null != mDownloadingDelay) {
mDownloadingDelay!!.cancel()
}
checkStopSelf()
}
private fun checkStopSelf() {
if (mDownloadManager == null || mDownloadManager!!.isIdle) {
stopForeground(true)
stopSelf()
}
}
private class NotificationDelay(
private var mService: Service,
private val mNotifyManager: NotificationManagerCompat,
private val mBuilder: NotificationCompat.Builder,
private val mId: Int
) : Runnable {
private var mLastTime: Long = 0
private var mPosted = false
// false for show, true for cancel
@Ops
private var mOps = 0
fun show() {
if (mPosted) {
mOps = OPS_NOTIFY
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_NOTIFY
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
mLastTime = now
}
}
fun cancel() {
if (mPosted) {
mOps = OPS_CANCEL
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mNotifyManager.cancel(mId)
} else {
// Too quick, post delay
mOps = OPS_CANCEL
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
fun startForeground() {
if (mPosted) {
mOps = OPS_START_FOREGROUND
} else {
val now = SystemClock.currentThreadTimeMillis()
if (now - mLastTime > DELAY) {
// Wait long enough, do it now
mService.startForeground(mId, mBuilder.build())
} else {
// Too quick, post delay
mOps = OPS_START_FOREGROUND
mPosted = true
SimpleHandler.getInstance().postDelayed(this, DELAY)
}
}
}
override fun run() {
mPosted = false
when (mOps) {
OPS_NOTIFY -> {
if (ActivityCompat.checkSelfPermission(
mService,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
mNotifyManager.notify(mId, mBuilder.build())
}
OPS_CANCEL -> mNotifyManager.cancel(mId)
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
}
}
@IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)
@Retention(AnnotationRetention.SOURCE)
private annotation class Ops
companion object {
private const val OPS_NOTIFY = 0
private const val OPS_CANCEL = 1
private const val OPS_START_FOREGROUND = 2
private const val DELAY: Long = 1000 // 1s
}
}
companion object {
const val ACTION_START = "start"
const val ACTION_START_RANGE = "start_range"
const val ACTION_START_ALL = "start_all"
const val ACTION_STOP = "stop"
const val ACTION_STOP_RANGE = "stop_range"
const val ACTION_STOP_CURRENT = "stop_current"
const val ACTION_STOP_ALL = "stop_all"
const val ACTION_DELETE = "delete"
const val ACTION_DELETE_RANGE = "delete_range"
const val ACTION_CLEAR = "clear"
const val KEY_GALLERY_INFO = "gallery_info"
const val KEY_LABEL = "label"
const val KEY_GID = "gid"
const val KEY_GID_LIST = "gid_list"
private const val ID_DOWNLOADING = 1
private const val ID_DOWNLOADED = 2
private const val ID_509 = 3
private val sItemStateArray = SparseJBArray()
private val sItemTitleArray = SparseJLArray<String>()
private var sFailedCount = 0
private var sFinishedCount = 0
private var sDownloadedCount = 0
@JvmStatic
fun clear() {
sFailedCount = 0
sFinishedCount = 0
sDownloadedCount = 0
sItemStateArray.clear()
sItemTitleArray.clear()
}
}
}
@@ -28,11 +28,13 @@ import com.hippo.UriArchiveAccessor;
import com.hippo.ehviewer.GetText;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.glgallery.GalleryPageView;
import com.hippo.image.Image;
import com.hippo.unifile.UniFile;
import com.hippo.yorozuya.thread.PVLock;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -47,14 +49,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public static PVLock pv = new PVLock(0);
private final UriArchiveAccessor archiveAccessor;
private final Stack<Integer> requests = new Stack<>();
private final LinkedHashMap<Integer, Long> streams = new LinkedHashMap<>();
private final Thread[] decodeThread = new Thread[]{
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask()),
new Thread(new DecodeTask())
};
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private final LinkedHashMap<Integer, InputStream> streams = new LinkedHashMap<>();
private final AtomicInteger decodingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
private Thread archiveThread;
private Thread decodeThread;
private volatile int size = STATE_WAIT;
private String error;
@@ -76,22 +75,25 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
int id = sIdGenerator.incrementAndGet();
archiveThread = new PriorityThread(
new ArchiveHostTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
new ArchiveTask(), "ArchiveTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
archiveThread.start();
for (Thread i : decodeThread) {
i.start();
}
decodeThread = new PriorityThread(
new DecodeTask(), "DecodeTask" + '-' + id, Process.THREAD_PRIORITY_BACKGROUND);
decodeThread.start();
}
@Override
public void stop() {
super.stop();
if (archiveThread != null) {
archiveThread.interrupt();
archiveThread = null;
}
for (Thread i : decodeThread) {
i.interrupt();
if (decodeThread != null) {
decodeThread.interrupt();
decodeThread = null;
}
try {
archiveAccessor.close();
@@ -109,11 +111,11 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
protected void onRequest(int index) {
boolean inDecodeTask;
synchronized (streams) {
inDecodeTask = streams.containsKey(index);
inDecodeTask = streams.containsKey(index) || index == decodingIndex.get();
}
synchronized (requests) {
boolean inArchiveTask = requests.contains(index);
boolean inArchiveTask = requests.contains(index) || index == extractingIndex.get();
if (!inArchiveTask && !inDecodeTask) {
requests.add(index);
requests.notify();
@@ -166,14 +168,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
return Integer.toString(index);
}
private class ArchiveHostTask implements Runnable {
public final Thread[] archiveThreads = new Thread[]{
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask()),
new Thread(new ArchiveTask())
};
private class ArchiveTask implements Runnable {
private void waitPasswd() throws InterruptedException {
showPasswd.sendEmptyMessage(0);
pv.p();
@@ -231,26 +226,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
}
}
}
for (Thread i : archiveThreads) {
i.start();
}
Object o = new Object();
synchronized (o) {
try {
o.wait();
} catch (InterruptedException e) {
for (Thread i : archiveThreads) {
i.interrupt();
}
}
}
}
}
private class ArchiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
synchronized (requests) {
@@ -264,26 +240,28 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
continue;
}
index = requests.pop();
extractingIndex.lazySet(index);
}
// Check index valid
if (index < 0 || index >= size) {
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
continue;
}
Pipe pipe = new Pipe();
synchronized (streams) {
if (streams.get(index) != null) {
continue;
}
}
long addr = archiveAccessor.extracttoOutputStream(index);
synchronized (streams) {
streams.put(index, addr);
streams.put(index, pipe.getInputStream());
streams.notify();
}
archiveAccessor.extracttoOutputStream(index, pipe.getOutputFd());
pipe.closeOutputFd();
}
}
}
@@ -293,29 +271,35 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
int index;
Long addr;
InputStream stream;
synchronized (streams) {
if (streams.isEmpty()) {
try {
streams.wait();
} catch (InterruptedException e) {
// Interrupted
break;
}
continue;
}
Iterator<Map.Entry<Integer, Long>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, Long> entry = iterator.next();
Iterator<Map.Entry<Integer, InputStream>> iterator = streams.entrySet().iterator();
Map.Entry<Integer, InputStream> entry = iterator.next();
iterator.remove();
index = entry.getKey();
addr = entry.getValue();
stream = entry.getValue();
decodingIndex.lazySet(index);
}
Image image = Image.decodeAddr(addr, true);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
try {
Image image = Image.decode(stream, true);
if (image != null) {
notifyPageSucceed(index, image);
} else {
notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed));
}
} finally {
decodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
}
}
}
@@ -34,7 +34,6 @@ import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.StringUtils;
import com.hippo.yorozuya.thread.PriorityThread;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -259,7 +258,7 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable {
InputStream is = null;
try {
is = files[index].openInputStream();
Image image = Image.decode(((FileInputStream) is).getFD(), true);
Image image = Image.decode(is, true);
mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX);
if (image != null) {
notifyPageSucceed(index, image);
@@ -0,0 +1,52 @@
/*
* Copyright 2019 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.gallery;
import android.os.ParcelFileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
class Pipe {
private ParcelFileDescriptor[] fd;
{
try {
fd = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
e.printStackTrace();
}
}
private final InputStream inputStream = new FileInputStream(fd[0].getFileDescriptor());
InputStream getInputStream() {
return inputStream;
}
int getOutputFd() {
return fd[1].getFd();
}
void closeOutputFd() {
try {
fd[1].close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,117 @@
/*
* Copyright 2018 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.preference;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.util.AttributeSet;
import androidx.appcompat.app.AlertDialog;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
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.preference.MessagePreference;
import com.hippo.util.ClipboardUtil;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class IdentityCookiePreference extends MessagePreference {
@SuppressLint("StaticFieldLeak")
private final SettingsActivity mActivity;
private String message;
public IdentityCookiePreference(Context context) {
super(context);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs) {
super(context, attrs);
mActivity = (SettingsActivity) context;
init();
}
public IdentityCookiePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mActivity = (SettingsActivity) context;
init();
}
private void init() {
EhCookieStore store = EhApplication.getEhCookieStore(getContext());
List<Cookie> eCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_E));
List<Cookie> exCookies = store.getCookies(HttpUrl.get(EhUrl.HOST_EX));
List<Cookie> cookies = new LinkedList<>(eCookies);
cookies.addAll(exCookies);
String ipbMemberId = null;
String ipbPassHash = null;
String igneous = null;
for (int i = 0, n = cookies.size(); i < n; i++) {
Cookie cookie = cookies.get(i);
switch (cookie.name()) {
case EhCookieStore.KEY_IPD_MEMBER_ID:
ipbMemberId = cookie.value();
break;
case EhCookieStore.KEY_IPD_PASS_HASH:
ipbPassHash = cookie.value();
break;
case EhCookieStore.KEY_IGNEOUS:
igneous = cookie.value();
break;
}
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous;
setDialogMessage(Html.fromHtml(getContext().getString(R.string.settings_eh_identity_cookies_signed, message), Html.FROM_HTML_MODE_LEGACY));
message = message.replace("<br>", "\n");
} else {
setDialogMessage(getContext().getString(R.string.settings_eh_identity_cookies_tourist));
}
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
if (message != null) {
builder.setPositiveButton(R.string.settings_eh_identity_cookies_copy, (dialog, which) -> {
ClipboardUtil.addTextToClipboard(message);
mActivity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT);
IdentityCookiePreference.this.onClick(dialog, which);
});
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setPositiveButton(android.R.string.ok, null);
}
}
}
@@ -1,104 +0,0 @@
/*
* Copyright 2018 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.preference
import android.content.*
import android.os.Build
import android.os.PersistableBundle
import android.text.Html
import android.util.AttributeSet
import androidx.appcompat.app.AlertDialog
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
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.preference.MessagePreference
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import java.util.*
class IdentityCookiePreference @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : MessagePreference(context, attrs) {
private val mActivity = context as SettingsActivity
private var message: String? = null
init {
val store = EhApplication.getEhCookieStore(context)
val eCookies = store.getCookies(EhUrl.HOST_E.toHttpUrl())
val exCookies = store.getCookies(EhUrl.HOST_EX.toHttpUrl())
val cookies: MutableList<Cookie> = LinkedList(eCookies)
cookies.addAll(exCookies)
var ipbMemberId: String? = null
var ipbPassHash: String? = null
var igneous: String? = null
var i = 0
val n = cookies.size
while (i < n) {
val cookie = cookies[i]
when (cookie.name) {
EhCookieStore.KEY_IPD_MEMBER_ID -> ipbMemberId = cookie.value
EhCookieStore.KEY_IPD_PASS_HASH -> ipbPassHash = cookie.value
EhCookieStore.KEY_IGNEOUS -> igneous = cookie.value
}
i++
}
if (ipbMemberId != null || ipbPassHash != null || igneous != null) {
message = (EhCookieStore.KEY_IPD_MEMBER_ID + ": " + ipbMemberId + "<br>"
+ EhCookieStore.KEY_IPD_PASS_HASH + ": " + ipbPassHash + "<br>"
+ EhCookieStore.KEY_IGNEOUS + ": " + igneous)
setDialogMessage(
Html.fromHtml(
context.getString(
R.string.settings_eh_identity_cookies_signed,
message
), Html.FROM_HTML_MODE_LEGACY
)
)
message = message!!.replace("<br>", "\n")
} else {
setDialogMessage(context.getString(R.string.settings_eh_identity_cookies_tourist))
}
}
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
if (message != null) {
builder.setPositiveButton(R.string.settings_eh_identity_cookies_copy) { dialog: DialogInterface?, which: Int ->
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
val clipData = ClipData.newPlainText(null, message)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
clipData.apply {
description.extras = PersistableBundle().apply {
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
}
}
clipboardManager?.setPrimaryClip(clipData)
// There is no need to notify user by toast since Tiramisu have its clipboard own logic
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
mActivity.showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)
this@IdentityCookiePreference.onClick(dialog, which)
}
builder.setNegativeButton(android.R.string.cancel, null)
} else {
builder.setPositiveButton(android.R.string.ok, null)
}
}
}
@@ -44,19 +44,20 @@ class VersionPreference @JvmOverloads constructor(
val info = NativeLibInfo().addLib(Native.getlibarchiveVersion())
.addLib("lzma(xz-utils)", Native.getliblzmaVersion())
.addLib("zlib", Native.getzlibVersion())
.addLib("libjpeg-turbo", Native.getlibjpeg_turboVersion())
.addLib(Native.getnettleVersion())
builder.setMessage(info.getMessage())
}
inner class NativeLibInfo {
private var message: String = ""
fun addLib(libname: String, libversion: String): NativeLibInfo {
fun addLib(libname:String, libversion:String): NativeLibInfo {
message = "$message$libname $libversion\n"
return this
}
fun getMessage(): String {
return message
return message;
}
fun addLib(msg: String): NativeLibInfo {
@@ -66,7 +66,6 @@ import com.hippo.yorozuya.thread.PriorityThread;
import com.hippo.yorozuya.thread.PriorityThreadFactory;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -1097,6 +1096,33 @@ public final class SpiderQueen implements Runnable {
void onGetImageFailure(int index, String error);
}
private static class AutoCloseInputStream extends InputStream {
private final InputStreamPipe mPipe;
private final InputStream mIs;
public AutoCloseInputStream(InputStreamPipe pipe, InputStream is) {
mPipe = pipe;
mIs = is;
}
@Override
public int read() throws IOException {
return mIs.read();
}
@Override
public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException {
return mIs.read(buffer, byteOffset, byteCount);
}
@Override
public void close() throws IOException {
mPipe.close();
mPipe.release();
}
}
private class SpiderWorker implements Runnable {
private final long mGid;
@@ -1667,7 +1693,7 @@ public final class SpiderQueen implements Runnable {
pipe.obtain();
try {
is = pipe.open();
is = new AutoCloseInputStream(pipe, pipe.open());
} catch (IOException e) {
// Can't open pipe
error = GetText.getString(R.string.error_reading_failed);
@@ -1677,17 +1703,7 @@ public final class SpiderQueen implements Runnable {
}
if (is != null) {
try {
image = Image.decode(((FileInputStream) is).getFD(), true);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
image = Image.decode(is, true);
if (image == null) {
error = GetText.getString(R.string.error_decoding_failed);
}
@@ -18,7 +18,6 @@ package com.hippo.ehviewer.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import androidx.core.content.ContextCompat;
@@ -43,7 +42,6 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class CommonOperations {
@@ -112,14 +110,8 @@ public final class CommonOperations {
startDownload(activity, Collections.singletonList(galleryInfo), forceDefault);
}
// TODO Add context if activity and context are different style
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();
}
doStartDownload(activity, galleryInfos, forceDefault);
}
private static void doStartDownload(final MainActivity activity, final List<GalleryInfo> galleryInfos, boolean forceDefault) {
final DownloadManager dm = EhApplication.getDownloadManager(activity);
LongList toStart = new LongList();
@@ -0,0 +1,134 @@
/*
* 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.ui;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatActivity;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import rikka.core.res.ResourcesKt;
import rikka.insets.WindowInsetsHelper;
import rikka.layoutinflater.view.LayoutInflaterFactory;
public abstract class EhActivity extends AppCompatActivity {
private static final String THEME_DEFAULT = "DEFAULT";
private static final String THEME_BLACK = "BLACK";
public static boolean isBlackNightTheme() {
return Settings.getBoolean("black_dark_theme", false);
}
public static String getTheme(Context context) {
if (isBlackNightTheme() && isNightMode(context.getResources().getConfiguration()))
return THEME_BLACK;
return THEME_DEFAULT;
}
public static boolean isNightMode(Configuration configuration) {
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
}
@StyleRes
public int getThemeStyleRes(Context context) {
if (THEME_BLACK.equals(getTheme(context))) {
return R.style.ThemeOverlay_Black;
}
return R.style.ThemeOverlay;
}
@Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
if (getParent() == null) {
theme.applyStyle(resid, true);
} else {
try {
theme.setTo(getParent().getTheme());
} catch (Exception ignored) {
}
theme.applyStyle(resid, false);
}
theme.applyStyle(getThemeStyleRes(this), true);
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first);
}
@Override
protected void onNightModeChanged(int mode) {
getTheme().applyStyle(getThemeStyleRes(this), true);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
getLayoutInflater().setFactory2((new LayoutInflaterFactory(getDelegate())).addOnViewCreatedListener(WindowInsetsHelper.getLISTENER()));
super.onCreate(savedInstanceState);
Window window = getWindow();
window.setStatusBarColor(Color.TRANSPARENT);
window.getDecorView().post(() -> {
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.setNavigationBarDividerColor(getColor(R.color.navigation_bar_divider));
}
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(false);
}
} else {
window.setNavigationBarColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(true);
}
}
});
((EhApplication) getApplication()).registerActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
((EhApplication) getApplication()).unregisterActivity(this);
}
@Override
protected void onResume() {
super.onResume();
if (Settings.getEnabledSecurity()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
}
@@ -1,151 +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.ui
import android.Manifest
import android.content.Context
import android.content.Intent
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
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.SecurityActivity.Companion.isAuthenticationSupported
import rikka.core.res.resolveColor
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class EhActivity : AppCompatActivity() {
@StyleRes
fun getThemeStyleRes(context: Context): Int {
return if (THEME_BLACK == getTheme(context)) {
R.style.ThemeOverlay_Black
} else R.style.ThemeOverlay
}
override fun onApplyThemeResource(theme: Theme, resid: Int, first: Boolean) {
if (parent == null) {
theme.applyStyle(resid, true)
} else {
try {
theme.setTo(parent.theme)
} catch (ignored: Exception) {
}
theme.applyStyle(resid, false)
}
theme.applyStyle(getThemeStyleRes(this), true)
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first)
}
override fun onNightModeChanged(mode: Int) {
theme.applyStyle(getThemeStyleRes(this), true)
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 =
LayoutInflaterFactory(delegate).addOnViewCreatedListener(
WindowInsetsHelper.LISTENER
)
super.onCreate(savedInstanceState)
val window = window
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.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)
}
override fun onDestroy() {
super.onDestroy()
(application as EhApplication).unregisterActivity(this)
}
override fun onResume() {
if (Settings.getSecurity() && isAuthenticationSupported(this) && EhApplication.locked) {
startActivity(Intent(this, SecurityActivity::class.java))
}
super.onResume()
if (Settings.getEnabledSecurity()) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {
Settings.putNotificationRequired()
}
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
fun checkAndRequestNotificationPermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
return
}
if (Settings.getNotificationRequired())
return
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
companion object {
private const val THEME_DEFAULT = "DEFAULT"
private const val THEME_BLACK = "BLACK"
private val isBlackNightTheme: Boolean
get() = Settings.getBoolean("black_dark_theme", false)
fun getTheme(context: Context): String {
return if (isBlackNightTheme && isNightMode(context.resources.configuration)) THEME_BLACK else THEME_DEFAULT
}
private fun isNightMode(configuration: Configuration): Boolean {
return configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0
}
}
}
@@ -226,9 +226,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
private int mSize;
private int mCurrentIndex;
private int mSavingPage = -1;
private EditTextDialogBuilder builder;
private boolean dialogShown = false;
private AlertDialog dialog;
private void buildProvider() {
if (mGalleryProvider != null) {
@@ -303,16 +300,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
break;
}
super.attachBaseContext(newBase);
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result && mSavingPage != -1) {
saveImage(mSavingPage);
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
}
mSavingPage = -1;
});
}
@Override
@SuppressWarnings({"WrongConstant"})
@@ -406,22 +394,17 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
switch (event.getAction()) {
case MotionEvent.ACTION_SCROLL:
float scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300;
if (scroll < 0.0f) {
if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
} else {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
} else {
if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {
mGalleryView.pageRight();
} else if (mLayoutMode == GalleryView.LAYOUT_LEFT_TO_RIGHT) {
} else {
mGalleryView.pageLeft();
} else if (mLayoutMode == GalleryView.LAYOUT_TOP_TO_BOTTOM) {
mGalleryView.onScroll(0, -scroll, 0, -scroll, 0, -scroll);
}
}
}
@@ -494,7 +477,16 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main);
mainLayout.addView(new GalleryGuideView(this));
}
}
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result && mSavingPage != -1) {
saveImage(mSavingPage);
} else {
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
}
mSavingPage = -1;
});
@Override
protected void onDestroy() {
@@ -1054,66 +1046,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
}
private void showPasswdDialog() {
if (!dialogShown) {
dialogShown = true;
dialog.show();
if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
GalleryActivity.this.onProvidePasswd();
});
}
dialog.setOnCancelListener(v -> finish());
}
}
private void onProvidePasswd() {
String passwd = builder.getText();
if (passwd.isEmpty())
builder.setError(getString(R.string.passwd_cannot_be_empty));
else {
ArchiveGalleryProvider.passwd = passwd;
ArchiveGalleryProvider.pv.v();
}
}
private void onPasswdWrong() {
builder.setError(getString(R.string.passwd_wrong));
}
private void onPasswdCorrect() {
dialog.dismiss();
}
private static class ShowPasswdDialogHandler extends Handler {
//弱引用持有HandlerActivity , GC 回收时会被回收掉
private final WeakReference<GalleryActivity> weakReference;
public ShowPasswdDialogHandler(GalleryActivity activity) {
this.weakReference = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
GalleryActivity activity = weakReference.get();
super.handleMessage(msg);
if (null != activity) {
switch (msg.what) {
case 0:
activity.showPasswdDialog();
break;
case 1:
activity.onPasswdWrong();
break;
case 2:
activity.onPasswdCorrect();
break;
}
}
}
}
private class GalleryMenuHelper implements DialogInterface.OnClickListener {
private final View mView;
@@ -1374,7 +1306,69 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
}
}
private EditTextDialogBuilder builder;
private boolean dialogShown = false;
private AlertDialog dialog;
private void showPasswdDialog() {
if (!dialogShown) {
dialogShown = true;
dialog.show();
if (dialog.getButton(AlertDialog.BUTTON_POSITIVE) != null) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
GalleryActivity.this.onProvidePasswd();
});
}
dialog.setOnCancelListener(v -> finish());
}
}
private void onProvidePasswd() {
String passwd = builder.getText();
if (passwd.isEmpty())
builder.setError(getString(R.string.passwd_cannot_be_empty));
else {
ArchiveGalleryProvider.passwd = passwd;
ArchiveGalleryProvider.pv.v();
}
}
private void onPasswdWrong() {
builder.setError(getString(R.string.passwd_wrong));
}
private void onPasswdCorrect() {
dialog.dismiss();
}
private static class ShowPasswdDialogHandler extends Handler {
//弱引用持有HandlerActivity , GC 回收时会被回收掉
private final WeakReference<GalleryActivity> weakReference;
public ShowPasswdDialogHandler(GalleryActivity activity) {
this.weakReference = new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
GalleryActivity activity = weakReference.get();
super.handleMessage(msg);
if (null != activity) {
switch (msg.what) {
case 0:
activity.showPasswdDialog();
break;
case 1:
activity.onPasswdWrong();
break;
case 2:
activity.onPasswdCorrect();
break;
}
}
}
}
}
@@ -47,6 +47,7 @@ import com.google.android.material.navigation.NavigationView;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.ehviewer.AppConfig;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhTagDatabase;
@@ -66,9 +67,11 @@ import com.hippo.ehviewer.ui.scene.GalleryListScene;
import com.hippo.ehviewer.ui.scene.GalleryPreviewsScene;
import com.hippo.ehviewer.ui.scene.HistoryScene;
import com.hippo.ehviewer.ui.scene.ProgressScene;
import com.hippo.ehviewer.ui.scene.SecurityScene;
import com.hippo.ehviewer.ui.scene.SelectSiteScene;
import com.hippo.ehviewer.ui.scene.SignInScene;
import com.hippo.ehviewer.ui.scene.SolidScene;
import com.hippo.ehviewer.ui.scene.WarningScene;
import com.hippo.ehviewer.ui.scene.WebViewSignInScene;
import com.hippo.ehviewer.widget.EhStageLayout;
import com.hippo.io.UniFileInputStreamPipe;
@@ -95,6 +98,8 @@ public final class MainActivity extends StageActivity
private static final String KEY_NAV_CHECKED_ITEM = "nav_checked_item";
static {
registerLaunchMode(SecurityScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(WarningScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(SignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(WebViewSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
registerLaunchMode(CookieSignInScene.class, SceneFragment.LAUNCH_MODE_SINGLE_TASK);
@@ -138,7 +143,11 @@ public final class MainActivity extends StageActivity
@NonNull
@Override
protected Announcer getLaunchAnnouncer() {
if (EhUtils.needSignedIn(this)) {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
return new Announcer(SecurityScene.class);
} else if (Settings.getShowWarning()) {
return new Announcer(WarningScene.class);
} else if (EhUtils.needSignedIn(this)) {
return new Announcer(SignInScene.class);
} else if (Settings.getSelectSite()) {
return new Announcer(SelectSiteScene.class);
@@ -152,7 +161,17 @@ public final class MainActivity extends StageActivity
// Sometimes scene can't show directly
private Announcer processAnnouncer(Announcer announcer) {
if (0 == getSceneCount()) {
if (EhUtils.needSignedIn(this)) {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
Bundle newArgs = new Bundle();
newArgs.putString(SecurityScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
newArgs.putBundle(SecurityScene.KEY_TARGET_ARGS, announcer.getArgs());
return new Announcer(SecurityScene.class).setArgs(newArgs);
} else if (Settings.getShowWarning()) {
Bundle newArgs = new Bundle();
newArgs.putString(WarningScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
newArgs.putBundle(WarningScene.KEY_TARGET_ARGS, announcer.getArgs());
return new Announcer(WarningScene.class).setArgs(newArgs);
} else if (EhUtils.needSignedIn(this)) {
Bundle newArgs = new Bundle();
newArgs.putString(SignInScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
newArgs.putBundle(SignInScene.KEY_TARGET_ARGS, announcer.getArgs());
@@ -288,6 +307,7 @@ public final class MainActivity extends StageActivity
mStageLayout = (EhStageLayout) ViewUtils.$$(this, R.id.fragment_container);
mDrawerLayout = (DrawerLayout) ViewUtils.$$(this, R.id.draw_view);
mDrawerLayout.setStatusBarBackgroundColor(0);
mNavView = (NavigationView) ViewUtils.$$(this, R.id.nav_view);
mRightDrawer = (DrawerView) ViewUtils.$$(this, R.id.right_drawer);
@@ -407,6 +427,9 @@ public final class MainActivity extends StageActivity
@Override
protected void onResume() {
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this) && EhApplication.locked) {
startScene(new Announcer(SecurityScene.class), true);
}
super.onResume();
setNavCheckedItem(mNavCheckedItem);
checkClipboardUrl();
@@ -0,0 +1,68 @@
/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.snackbar.Snackbar;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.ui.fragment.SettingsFragment;
import com.hippo.ehviewer.ui.scene.BaseScene;
public final class SettingsActivity extends EhActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preference);
setSupportActionBar(findViewById(R.id.toolbar));
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setDisplayHomeAsUpEnabled(true);
}
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
.replace(R.id.fragment, new SettingsFragment())
.commitAllowingStateLoss();
}
}
public void showTip(@StringRes int id, int length) {
showTip(getString(id), length);
}
public void showTip(CharSequence message, int length) {
Snackbar.make(findViewById(R.id.snackbar), message,
length == BaseScene.LENGTH_LONG ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -1,65 +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.ui
import android.content.res.Configuration
import android.os.Bundle
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.core.view.WindowCompat
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.snackbar.Snackbar
import com.hippo.ehviewer.R
import com.hippo.ehviewer.ui.fragment.SettingsFragment
import com.hippo.ehviewer.ui.scene.BaseScene
class SettingsActivity : EhActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preference)
setSupportActionBar(findViewById(R.id.toolbar))
val bar = supportActionBar
bar?.setDisplayHomeAsUpEnabled(true)
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) <= 0
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
.replace(R.id.fragment, SettingsFragment())
.commitAllowingStateLoss()
}
}
fun showTip(@StringRes id: Int, length: Int) {
showTip(getString(id), length)
}
fun showTip(message: CharSequence?, length: Int) {
Snackbar.make(
findViewById(R.id.snackbar), message!!,
if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
).show()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
}
@@ -22,6 +22,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -227,8 +228,6 @@ public class AdvancedFragment extends BasePreferenceFragment {
Preference dumpLogcat = findPreference(KEY_DUMP_LOGCAT);
Preference clearMemoryCache = findPreference(KEY_CLEAR_MEMORY_CACHE);
Preference appLanguage = findPreference(KEY_APP_LANGUAGE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
appLanguage.setVisible(false);
Preference importData = findPreference(KEY_IMPORT_DATA);
Preference exportData = findPreference(KEY_EXPORT_DATA);
Preference openByDefault = findPreference(KEY_OPEN_BY_DEFAULT);
@@ -296,7 +295,7 @@ public class AdvancedFragment extends BasePreferenceFragment {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey();
if (KEY_APP_LANGUAGE.equals(key) && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (KEY_APP_LANGUAGE.equals(key)) {
if ("system".equals(newValue)) {
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
} else {
@@ -70,8 +70,7 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
.replace(R.id.fragment, classObj)
.addToBackStack(null)
.commitAllowingStateLoss();
} catch (ClassNotFoundException | IllegalAccessException |
java.lang.InstantiationException ignored) {
} catch (ClassNotFoundException | IllegalAccessException | java.lang.InstantiationException ignored) {
}
return true;
}
@@ -38,7 +38,6 @@ public class EhFragment extends BasePreferenceFragment {
Preference blackDarkTheme = findPreference(Settings.KEY_BLACK_DARK_THEME);
Preference gallerySite = findPreference(Settings.KEY_GALLERY_SITE);
Preference listMode = findPreference(Settings.KEY_LIST_MODE);
Preference listThumbSize = findPreference(Settings.KEY_LIST_THUMB_SIZE);
Preference detailSize = findPreference(Settings.KEY_DETAIL_SIZE);
Preference thumbSize = findPreference(Settings.KEY_THUMB_SIZE);
Preference showTagTranslations = findPreference(Settings.KEY_SHOW_TAG_TRANSLATIONS);
@@ -47,7 +46,6 @@ public class EhFragment extends BasePreferenceFragment {
theme.setOnPreferenceChangeListener(this);
gallerySite.setOnPreferenceChangeListener(this);
listMode.setOnPreferenceChangeListener(this);
listThumbSize.setOnPreferenceChangeListener(this);
detailSize.setOnPreferenceChangeListener(this);
thumbSize.setOnPreferenceChangeListener(this);
showTagTranslations.setOnPreferenceChangeListener(this);
@@ -72,10 +70,6 @@ public class EhFragment extends BasePreferenceFragment {
} else if (Settings.KEY_LIST_MODE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
return true;
} else if (Settings.KEY_LIST_THUMB_SIZE.equals(key)) {
Settings.LIST_THUMB_SIZE_INITED = false;
requireActivity().setResult(Activity.RESULT_OK);
return true;
} else if (Settings.KEY_DETAIL_SIZE.equals(key)) {
requireActivity().setResult(Activity.RESULT_OK);
} else if (Settings.KEY_THUMB_SIZE.equals(key)) {
@@ -22,7 +22,7 @@ import android.os.Bundle
import androidx.preference.SwitchPreferenceCompat
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.ui.SecurityActivity.Companion.isAuthenticationSupported
import com.hippo.ehviewer.ui.scene.SecurityScene.Companion.isAuthenticationSupported
/**
* Created by Mo10 on 2018/2/10.
@@ -56,6 +56,7 @@ public abstract class BaseScene extends SceneFragment {
private View drawerView;
@Nullable
private SparseArray<Parcelable> drawerViewState;
private boolean needWhiteStatusBar = needWhiteStatusBar();
public void addAboveSnackView(View view) {
FragmentActivity activity = getActivity();
@@ -117,6 +118,10 @@ public abstract class BaseScene extends SceneFragment {
return true;
}
public boolean needWhiteStatusBar() {
return true;
}
public int getNavCheckedItem() {
return 0;
}
@@ -171,6 +176,14 @@ public abstract class BaseScene extends SceneFragment {
drawerView = null;
}
public void setLightStatusBar(boolean set) {
var insetsController = getInsetsController();
if (insetsController != null) {
insetsController.setAppearanceLightStatusBars(set && (requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) <= 0);
}
needWhiteStatusBar = set;
}
public void onDestroyDrawerView() {
}
@@ -200,11 +213,7 @@ public abstract class BaseScene extends SceneFragment {
// Hide soft ime
hideSoftInput();
insetsController = getInsetsController();
if (insetsController != null) {
insetsController.setAppearanceLightStatusBars((requireActivity().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) <= 0);
}
setLightStatusBar(needWhiteStatusBar);
}
@Nullable
@@ -22,17 +22,21 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
@@ -41,18 +45,26 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder;
import com.hippo.app.CheckBoxDialogBuilder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.conaco.DataContainer;
@@ -73,6 +85,7 @@ import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.download.DownloadService;
import com.hippo.ehviewer.spider.SpiderDen;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.io.UniFileInputStreamPipe;
import com.hippo.scene.Announcer;
@@ -86,6 +99,7 @@ import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.FileUtils;
import com.hippo.yorozuya.IOUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.ViewUtils;
import com.hippo.yorozuya.collect.LongList;
@@ -376,9 +390,90 @@ public class DownloadsScene extends ToolbarScene
updateView();
guide();
return view;
}
private void guide() {
if (Settings.getGuideDownloadThumb() && null != mRecyclerView) {
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Settings.getGuideDownloadThumb()) {
guideDownloadThumb();
}
if (null != mRecyclerView) {
ViewUtils.removeOnGlobalLayoutListener(mRecyclerView.getViewTreeObserver(), this);
}
}
});
} else {
guideDownloadLabels();
}
}
private void guideDownloadThumb() {
MainActivity activity = getMainActivity();
if (null == activity || !Settings.getGuideDownloadThumb() || null == mLayoutManager || null == mRecyclerView) {
guideDownloadLabels();
return;
}
int position = mLayoutManager.findFirstCompletelyVisibleItemPositions(null)[0];
if (position < 0) {
guideDownloadLabels();
return;
}
RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(position);
if (null == holder) {
guideDownloadLabels();
return;
}
TapTargetView.showFor(requireActivity(),
TapTarget.forView(((DownloadHolder) holder).thumb,
getString(R.string.guide_download_thumb_title),
getString(R.string.guide_download_thumb_text))
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetDismissed(view, userInitiated);
Settings.putGuideDownloadThumb(false);
guideDownloadLabels();
}
});
}
private void guideDownloadLabels() {
MainActivity activity = getMainActivity();
if (null == activity || !Settings.getGuideDownloadLabels()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_download_labels_title),
getString(R.string.guide_download_labels_text))
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.puttGuideDownloadLabels(false);
openDrawer(Gravity.RIGHT);
}
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@@ -544,13 +639,20 @@ public class DownloadsScene extends ToolbarScene
initLabels();
RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager();
dragDropManager.setInitiateOnLongPress(true);
dragDropManager.setInitiateOnTouch(false);
dragDropManager.setDraggingItemAlpha(0.8f);
mLabelAdapter = new DownloadLabelAdapter(inflater);
final EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
mLabelAdapter.setHasStableIds(true);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new DownloadLabelItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView);
recyclerView.setAdapter(mLabelAdapter);
final GeneralItemAnimator animator = new DraggableItemAnimator();
recyclerView.setItemAnimator(animator);
recyclerView.setAdapter(dragDropManager.createWrappedAdapter(mLabelAdapter));
dragDropManager.attachRecyclerView(recyclerView);
return view;
}
@@ -873,7 +975,7 @@ public class DownloadsScene extends ToolbarScene
holder.speed.setText(FileUtils.humanReadableByteCount(speed, false) + "/S");
}
private static class DownloadLabelHolder extends RecyclerView.ViewHolder {
private static class DownloadLabelHolder extends AbstractDraggableItemViewHolder {
private final TextView label;
private final ImageView option;
@@ -952,7 +1054,7 @@ public class DownloadsScene extends ToolbarScene
}
}
private class DownloadLabelAdapter extends RecyclerView.Adapter<DownloadLabelHolder> {
private class DownloadLabelAdapter extends RecyclerView.Adapter<DownloadLabelHolder> implements DraggableItemAdapter<DownloadLabelHolder> {
private final LayoutInflater mInflater;
@@ -1006,15 +1108,36 @@ public class DownloadsScene extends ToolbarScene
});
if (position > 0) {
holder.option.setVisibility(View.VISIBLE);
holder.option.setOnClickListener(v -> {
holder.itemView.setOnLongClickListener(v -> {
if (context != null) {
EditTextDialogBuilder builder = new EditTextDialogBuilder(
context, label, getString(R.string.download_labels));
builder.setTitle(R.string.rename_label_title);
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.show();
new RenameLabelDialogHelper(builder, dialog, label);
PopupMenu popupMenu = new PopupMenu(context, holder.option);
popupMenu.inflate(R.menu.download_label_option);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.menu_label_rename) {
EditTextDialogBuilder builder = new EditTextDialogBuilder(
context, label, getString(R.string.download_labels));
builder.setTitle(R.string.rename_label_title);
builder.setPositiveButton(android.R.string.ok, null);
AlertDialog dialog = builder.show();
new RenameLabelDialogHelper(builder, dialog, label);
} else if (itemId == R.id.menu_label_remove) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_label_title))
.setMessage(getString(R.string.delete_label_message, label))
.setPositiveButton(R.string.delete, (dialog, which) -> {
mDownloadManager.deleteLabel(label);
mLabels.remove(position);
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
return false;
});
}
return true;
});
} else {
holder.option.setVisibility(View.GONE);
@@ -1031,6 +1154,43 @@ public class DownloadsScene extends ToolbarScene
public int getItemCount() {
return mLabels != null ? mLabels.size() : 0;
}
@Override
public boolean onCheckCanStartDrag(@NonNull DownloadLabelHolder holder, int position, int x, int y) {
return position != 0 && x > holder.option.getX() && y > holder.option.getY();
}
@Override
public ItemDraggableRange onGetItemDraggableRange(@NonNull DownloadLabelHolder holder, int position) {
return new ItemDraggableRange(1, getItemCount() - 1);
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
Context context = getContext();
if (null == context || fromPosition == toPosition || toPosition == 0) {
return;
}
EhApplication.getDownloadManager(context).moveLabel(fromPosition - 1, toPosition - 1);
final String item = mLabels.remove(fromPosition);
mLabels.add(toPosition, item);
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return dropPosition != 0;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
}
private class DeleteRangeDialogHelper implements DialogInterface.OnClickListener {
@@ -1368,43 +1528,4 @@ public class DownloadsScene extends ToolbarScene
}
}
}
private class DownloadLabelItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT);
}
@SuppressLint("NotifyDataSetChanged")
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
Context context = getContext();
if (null == context || fromPosition == toPosition || toPosition == 0 || fromPosition == 0) {
return false;
}
EhApplication.getDownloadManager(context).moveLabel(fromPosition - 1, toPosition - 1);
final String item = mLabels.remove(fromPosition);
mLabels.add(toPosition, item);
mLabelAdapter.notifyDataSetChanged();
return true;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getBindingAdapterPosition();
if (mDownloadManager == null)
return;
if (position != 0) {
String label = mLabels.get(position);
mDownloadManager.deleteLabel(label);
}
mLabels.remove(position);
mLabelAdapter.notifyDataSetChanged();
}
}
}
@@ -21,10 +21,13 @@ import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -42,6 +45,8 @@ import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hippo.annotation.Implemented;
@@ -74,6 +79,7 @@ import com.hippo.widget.ContentLayout;
import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.SimpleHandler;
import com.hippo.yorozuya.ViewUtils;
@@ -305,9 +311,40 @@ public class FavoritesScene extends BaseScene implements
mHelper.firstRefresh();
}
guideCollections();
return view;
}
private void guideCollections() {
Activity activity = getMainActivity();
if (null == activity || !Settings.getGuideCollections()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_collections_title),
getString(R.string.guide_collections_text))
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.putGuideCollections(false);
openDrawer(Gravity.RIGHT);
}
});
}
// keyword of mUrlBuilder, fav cat of mUrlBuilder, mFavCatArray.
// They changed, call it
private void updateSearchBar() {
@@ -559,6 +559,12 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
ViewGroup main = (ViewGroup) ViewUtils.$$(view, R.id.main);
ScrollView mainView = (ScrollView) ViewUtils.$$(main, R.id.scroll_view);
mainView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
if (mActionGroup != null && mHeader != null) {
//noinspection IntegerDivisionInFloatingPointContext
setLightStatusBar(mActionGroup.getY() - mHeader.findViewById(R.id.header_content).getPaddingTop() / 2 < scrollY);
}
});
View progressView = ViewUtils.$$(main, R.id.progress_view);
mTip = (TextView) ViewUtils.$$(main, R.id.tip);
mViewTransition = new ViewTransition(mainView, progressView, mTip);
@@ -662,6 +668,11 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
return view;
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -830,16 +841,19 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
switch (state) {
case STATE_NORMAL:
setLightStatusBar(false);
// Show mMainView
mViewTransition.showView(0, animation);
// Show mBelowHeader
mViewTransition2.showView(0, animation);
break;
case STATE_REFRESH:
setLightStatusBar(true);
// Show mProgressView
mViewTransition.showView(1, animation);
break;
case STATE_REFRESH_HEADER:
setLightStatusBar(false);
// Show mMainView
mViewTransition.showView(0, animation);
// Show mProgress
@@ -848,6 +862,7 @@ public class GalleryDetailScene extends ToolbarScene implements View.OnClickList
default:
case STATE_INIT:
case STATE_FAILED:
setLightStatusBar(true);
// Show mFailedView
mViewTransition.showView(2, animation);
break;
@@ -18,10 +18,13 @@ package com.hippo.ehviewer.ui.scene;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
@@ -30,8 +33,10 @@ import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
@@ -40,23 +45,30 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
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;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.h6ah4i.android.widget.advrecyclerview.animator.DraggableItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder;
import com.hippo.app.EditTextDialogBuilder;
import com.hippo.drawable.AddDeleteDrawable;
import com.hippo.drawable.DrawerArrowDrawable;
@@ -99,6 +111,7 @@ import com.hippo.widget.FabLayout;
import com.hippo.widget.SearchBarMover;
import com.hippo.yorozuya.AnimationUtils;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.MathUtils;
import com.hippo.yorozuya.SimpleAnimatorListener;
import com.hippo.yorozuya.StringUtils;
@@ -134,8 +147,6 @@ public final class GalleryListScene extends BaseScene
private final static int STATE_SEARCH = 2;
private final static int STATE_SEARCH_SHOW_LIST = 3;
private static final long ANIMATE_TIME = 300L;
// Double click back exit
private final long mPressBackTime = 0;
/*---------------
Whole life cycle
---------------*/
@@ -152,8 +163,8 @@ public final class GalleryListScene extends BaseScene
private EasyRecyclerView mRecyclerView;
@Nullable
private SearchLayout mSearchLayout;
ActivityResultLauncher<PickVisualMediaRequest> selectImageLauncher = registerForActivityResult(
new ActivityResultContracts.PickVisualMedia(),
ActivityResultLauncher<String[]> selectImageLauncher = registerForActivityResult(
new ActivityResultContracts.OpenDocument(),
result -> mSearchLayout.setImageUri(result));
@Nullable
private SearchBar mSearchBar;
@@ -216,6 +227,8 @@ public final class GalleryListScene extends BaseScene
}
}
};
// Double click back exit
private long mPressBackTime = 0;
private boolean mHasFirstRefresh = false;
private int mNavCheckedId = 0;
private DownloadManager mDownloadManager;
@@ -621,9 +634,40 @@ public final class GalleryListScene extends BaseScene
mHelper.firstRefresh();
}
guideQuickSearch();
return view;
}
private void guideQuickSearch() {
Activity activity = getMainActivity();
if (null == activity || !Settings.getGuideQuickSearch()) {
return;
}
Display display = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
display.getSize(point);
Rect bounds = new Rect(point.x + LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 + LayoutUtils.dp2pix(requireContext(), 20),
point.x - LayoutUtils.dp2pix(requireContext(), 20),
point.y / 3 - LayoutUtils.dp2pix(requireContext(), 20));
TapTargetView.showFor(requireActivity(),
TapTarget.forBounds(bounds,
getString(R.string.guide_quick_search_title),
getString(R.string.guide_quick_search_text))
.transparentTarget(true),
new TapTargetView.Listener() {
@Override
public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
super.onTargetClick(view);
Settings.putGuideQuickSearch(false);
openDrawer(Gravity.RIGHT);
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
@@ -742,13 +786,19 @@ public final class GalleryListScene extends BaseScene
Context context = getContext();
AssertUtils.assertNotNull(context);
RecyclerViewDragDropManager dragDropManager = new RecyclerViewDragDropManager();
dragDropManager.setInitiateOnLongPress(true);
dragDropManager.setInitiateOnTouch(false);
dragDropManager.setDraggingItemAlpha(0.8f);
final EasyRecyclerView recyclerView = view.findViewById(R.id.recycler_view_drawer);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
final QsDrawerAdapter qsDrawerAdapter = new QsDrawerAdapter(inflater);
qsDrawerAdapter.setHasStableIds(true);
recyclerView.setAdapter(qsDrawerAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GalleryListQSItemTouchHelperCallback(qsDrawerAdapter));
itemTouchHelper.attachToRecyclerView(recyclerView);
final GeneralItemAnimator animator = new DraggableItemAnimator();
recyclerView.setItemAnimator(animator);
recyclerView.setAdapter(dragDropManager.createWrappedAdapter(qsDrawerAdapter));
dragDropManager.attachRecyclerView(recyclerView);
mQuickSearchList = EhDB.getAllQuickSearch();
tip.setText(R.string.quick_search_tip);
if (mIsTopList) {
@@ -1335,9 +1385,7 @@ public final class GalleryListScene extends BaseScene
@Override
public void onSelectImage() {
try {
PickVisualMediaRequest.Builder builder = new PickVisualMediaRequest.Builder();
builder.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE);
selectImageLauncher.launch(builder.build());
selectImageLauncher.launch(new String[]{"image/*"});
} catch (Throwable e) {
ExceptionUtils.throwIfFatal(e);
showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT);
@@ -1477,7 +1525,7 @@ public final class GalleryListScene extends BaseScene
}
}
private static class QsDrawerHolder extends RecyclerView.ViewHolder {
private static class QsDrawerHolder extends AbstractDraggableItemViewHolder {
private final TextView key;
private final ImageView option;
@@ -1522,7 +1570,7 @@ public final class GalleryListScene extends BaseScene
}
}
private class QsDrawerAdapter extends RecyclerView.Adapter<QsDrawerHolder> {
private class QsDrawerAdapter extends RecyclerView.Adapter<QsDrawerHolder> implements DraggableItemAdapter<QsDrawerHolder> {
private final LayoutInflater mInflater;
@@ -1553,6 +1601,34 @@ public final class GalleryListScene extends BaseScene
setState(STATE_NORMAL);
closeDrawer(Gravity.RIGHT);
});
holder.itemView.setOnLongClickListener(v -> {
PopupMenu popupMenu = new PopupMenu(requireContext(), holder.option);
popupMenu.inflate(R.menu.quicksearch_option);
popupMenu.show();
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
final QuickSearch quickSearch = mQuickSearchList.get(position);
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_qs_remove) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_quick_search_title))
.setMessage(getString(R.string.delete_quick_search_message, quickSearch.name))
.setPositiveButton(R.string.delete, (dialog, which) -> {
EhDB.deleteQuickSearch(quickSearch);
mQuickSearchList.remove(position);
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
}
return false;
}
});
return true;
});
} else {
int[] keywords = {11, 12, 13, 15};
int[] toplists = {R.string.toplist_alltime, R.string.toplist_pastyear, R.string.toplist_pastmonth, R.string.toplist_yesterday};
@@ -1588,6 +1664,44 @@ public final class GalleryListScene extends BaseScene
public int getItemCount() {
return !mIsTopList ? mQuickSearchList != null ? mQuickSearchList.size() : 0 : 4;
}
@Override
public boolean onCheckCanStartDrag(@NonNull QsDrawerHolder holder, int position, int x, int y) {
return !mIsTopList && x > holder.option.getX() && y > holder.option.getY();
}
@Override
public ItemDraggableRange onGetItemDraggableRange(@NonNull QsDrawerHolder holder, int position) {
return null;
}
@Override
public void onMoveItem(int fromPosition, int toPosition) {
if (fromPosition == toPosition) {
return;
}
if (null == mQuickSearchList) {
return;
}
EhDB.moveQuickSearch(fromPosition, toPosition);
final QuickSearch item = mQuickSearchList.remove(fromPosition);
mQuickSearchList.add(toPosition, item);
}
@Override
public boolean onCheckCanDrop(int draggingPosition, int dropPosition) {
return true;
}
@Override
public void onItemDragStarted(int position) {
notifyDataSetChanged();
}
@Override
public void onItemDragFinished(int fromPosition, int toPosition, boolean result) {
notifyDataSetChanged();
}
}
private abstract class UrlSuggestion extends SearchBar.Suggestion {
@@ -1772,47 +1886,4 @@ public final class GalleryListScene extends BaseScene
}
}
}
private class GalleryListQSItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final QsDrawerAdapter mAdapter;
public GalleryListQSItemTouchHelperCallback(QsDrawerAdapter adapter) {
mAdapter = adapter;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT);
}
@SuppressLint("NotifyDataSetChanged")
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
if (fromPosition == toPosition) {
return false;
}
if (null == mQuickSearchList) {
return false;
}
EhDB.moveQuickSearch(fromPosition, toPosition);
final QuickSearch item = mQuickSearchList.remove(fromPosition);
mQuickSearchList.add(toPosition, item);
mAdapter.notifyDataSetChanged();
return true;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if (mQuickSearchList == null)
return;
int position = viewHolder.getBindingAdapterPosition();
final QuickSearch quickSearch = mQuickSearchList.get(position);
EhDB.deleteQuickSearch(quickSearch);
mQuickSearchList.remove(position);
mAdapter.notifyDataSetChanged();
}
}
}
@@ -0,0 +1,704 @@
/*
* 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.ui.scene;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionDefault;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem;
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.FastScroller;
import com.hippo.easyrecyclerview.HandlerDrawable;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.ehviewer.EhApplication;
import com.hippo.ehviewer.EhDB;
import com.hippo.ehviewer.FavouriteStatusRouter;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhCacheKeyFactory;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.ehviewer.client.data.GalleryInfo;
import com.hippo.ehviewer.dao.DownloadInfo;
import com.hippo.ehviewer.dao.DownloadLabel;
import com.hippo.ehviewer.dao.HistoryInfo;
import com.hippo.ehviewer.download.DownloadManager;
import com.hippo.ehviewer.ui.CommonOperations;
import com.hippo.ehviewer.ui.GalleryActivity;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter;
import com.hippo.ehviewer.widget.SimpleRatingView;
import com.hippo.scene.Announcer;
import com.hippo.scene.SceneFragment;
import com.hippo.view.ViewTransition;
import com.hippo.widget.LoadImageView;
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager;
import com.hippo.yorozuya.AssertUtils;
import com.hippo.yorozuya.ViewUtils;
import org.greenrobot.greendao.query.LazyList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rikka.core.res.ResourcesKt;
public class HistoryScene extends ToolbarScene {
/*---------------
View life cycle
---------------*/
@Nullable
private TextView mTip;
@Nullable
private FastScroller mFastScroller;
@Nullable
private EasyRecyclerView mRecyclerView;
@Nullable
private ViewTransition mViewTransition;
@Nullable
private RecyclerView.Adapter<?> mAdapter;
@Nullable
private LazyList<HistoryInfo> mLazyList;
private DownloadManager mDownloadManager;
private DownloadManager.DownloadInfoListener mDownloadInfoListener;
private FavouriteStatusRouter mFavouriteStatusRouter;
private FavouriteStatusRouter.Listener mFavouriteStatusRouterListener;
@Override
public void onDestroy() {
super.onDestroy();
mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener);
mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener);
}
@Override
public int getNavCheckedItem() {
return R.id.nav_history;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context context = getContext();
assert context != null;
mDownloadManager = EhApplication.getDownloadManager(context);
mDownloadManager = EhApplication.getDownloadManager(context);
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context);
mDownloadInfoListener = new DownloadManager.DownloadInfoListener() {
@Override
public void onAdd(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list, int position) {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onUpdate(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list) {
}
@Override
public void onUpdateAll() {
}
@Override
public void onReload() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onChange() {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onRenameLabel(String from, String to) {
}
@Override
public void onRemove(@NonNull DownloadInfo info, @NonNull List<DownloadInfo> list, int position) {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
@Override
public void onUpdateLabels() {
}
};
mDownloadManager.addDownloadInfoListener(mDownloadInfoListener);
mFavouriteStatusRouterListener = (gid, slot) -> {
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
};
mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener);
}
@Nullable
@Override
public View onCreateViewWithToolbar(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_history, container, false);
View content = ViewUtils.$$(view, R.id.content);
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(content, R.id.recycler_view);
mFastScroller = (FastScroller) ViewUtils.$$(content, R.id.fast_scroller);
mTip = (TextView) ViewUtils.$$(view, R.id.tip);
mViewTransition = new ViewTransition(content, mTip);
Context context = getContext();
AssertUtils.assertNotNull(context);
Resources resources = context.getResources();
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.big_history);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
mTip.setCompoundDrawables(null, drawable, null, null);
RecyclerViewTouchActionGuardManager guardManager = new RecyclerViewTouchActionGuardManager();
guardManager.setInterceptVerticalScrollingWhileAnimationRunning(true);
guardManager.setEnabled(true);
RecyclerViewSwipeManager swipeManager = new RecyclerViewSwipeManager();
mAdapter = new HistoryAdapter();
mAdapter.setHasStableIds(true);
mAdapter = swipeManager.createWrappedAdapter(mAdapter);
mRecyclerView.setAdapter(mAdapter);
final GeneralItemAnimator animator = new SwipeDismissItemAnimator();
animator.setSupportsChangeAnimations(false);
mRecyclerView.setItemAnimator(animator);
AutoStaggeredGridLayoutManager layoutManager = new AutoStaggeredGridLayoutManager(
0, StaggeredGridLayoutManager.VERTICAL);
layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId()));
layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setClipToPadding(false);
mRecyclerView.setClipChildren(false);
int interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval);
int paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h);
int paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v);
MarginItemDecoration decoration = new MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV);
mRecyclerView.addItemDecoration(decoration);
guardManager.attachRecyclerView(mRecyclerView);
swipeManager.attachRecyclerView(mRecyclerView);
mFastScroller.attachToRecyclerView(mRecyclerView);
HandlerDrawable handlerDrawable = new HandlerDrawable();
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
mFastScroller.setHandlerDrawable(handlerDrawable);
updateLazyList();
updateView(false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setTitle(R.string.history);
setNavigationIcon(R.drawable.ic_baseline_menu_24);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (null != mLazyList) {
mLazyList.close();
mLazyList = null;
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
}
if (null != mRecyclerView) {
mRecyclerView.stopScroll();
mRecyclerView = null;
}
mViewTransition = null;
mAdapter = null;
}
// Remember to notify
private void updateLazyList() {
LazyList<HistoryInfo> lazyList = EhDB.getHistoryLazyList();
if (mLazyList != null) {
mLazyList.close();
}
mLazyList = lazyList;
}
private void updateView(boolean animation) {
if (null == mAdapter || null == mViewTransition) {
return;
}
if (mAdapter.getItemCount() == 0) {
mViewTransition.showView(1, animation);
} else {
mViewTransition.showView(0, animation);
}
}
@SuppressLint("RtlHardcoded")
@Override
public void onNavigationClick() {
toggleDrawer(Gravity.LEFT);
}
@Override
public int getMenuResId() {
return R.menu.scene_history;
}
private void showClearAllDialog() {
new MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.clear_all_history)
.setPositiveButton(R.string.clear_all, (dialog, which) -> {
if (DialogInterface.BUTTON_POSITIVE != which || null == mAdapter) {
return;
}
EhDB.clearHistoryInfo();
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
// Skip when in choice mode
Context context = getContext();
if (null == context) {
return false;
}
int id = item.getItemId();
if (id == R.id.action_clear_all) {
showClearAllDialog();
return true;
}
return false;
}
public boolean onItemClick(View view, int position) {
if (null == mLazyList) {
return false;
}
Bundle args = new Bundle();
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mLazyList.get(position));
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
startScene(announcer);
return true;
}
public boolean onItemLongClick(int position) {
final Context context = getContext();
final MainActivity activity = getMainActivity();
if (null == context || null == activity || null == mLazyList) {
return false;
}
final GalleryInfo gi = mLazyList.get(position);
if (gi == null) {
return true;
}
boolean downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID;
boolean favourited = gi.favoriteSlot != -2;
CharSequence[] items = downloaded ? new CharSequence[]{
context.getString(R.string.read),
context.getString(R.string.delete_downloads),
context.getString(favourited ? R.string.remove_from_favourites : R.string.add_to_favourites),
context.getString(R.string.delete),
context.getString(R.string.download_move_dialog_title),
} : new CharSequence[]{
context.getString(R.string.read),
context.getString(R.string.download),
context.getString(favourited ? R.string.remove_from_favourites : R.string.add_to_favourites),
context.getString(R.string.delete),
};
int[] icons = downloaded ? new int[]{
R.drawable.v_book_open_x24,
R.drawable.v_delete_x24,
favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24,
R.drawable.v_delete_x24,
R.drawable.v_folder_move_x24,
} : new int[]{
R.drawable.v_book_open_x24,
R.drawable.v_download_x24,
favourited ? R.drawable.v_heart_broken_x24 : R.drawable.v_heart_x24,
R.drawable.v_delete_x24,
};
new MaterialAlertDialogBuilder(context)
.setTitle(EhUtils.getSuitableTitle(gi))
.setAdapter(new SelectItemWithIconAdapter(context, items, icons), (dialog, which) -> {
switch (which) {
case 0: // Read
Intent intent = new Intent(activity, GalleryActivity.class);
intent.setAction(GalleryActivity.ACTION_EH);
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi);
startActivity(intent);
break;
case 1: // Download
if (downloaded) {
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(getString(R.string.download_remove_dialog_message, gi.title))
.setPositiveButton(android.R.string.ok, (dialog1, which1) -> mDownloadManager.deleteDownload(gi.gid))
.show();
} else {
CommonOperations.startDownload(activity, gi, false);
}
break;
case 2: // Favorites
if (favourited) {
CommonOperations.removeFromFavorites(activity, gi, new RemoveFromFavoriteListener(context, activity.getStageId(), getTag()));
} else {
CommonOperations.addToFavorites(activity, gi, new AddToFavoriteListener(context, activity.getStageId(), getTag()));
}
break;
case 3: // Delete
if (null == mLazyList || null == mAdapter) {
return;
}
HistoryInfo info = mLazyList.get(position);
EhDB.deleteHistoryInfo(info);
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
break;
case 4: // Move
List<DownloadLabel> labelRawList = EhApplication.getDownloadManager(context).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++) {
labelList.add(labelRawList.get(i).getLabel());
}
String[] labels = labelList.toArray(new String[0]);
MoveDialogHelper helper = new MoveDialogHelper(labels, gi);
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show();
break;
}
}).show();
return true;
}
private static class AddToFavoriteListener extends EhCallback<GalleryListScene, Void> {
public AddToFavoriteListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(Void result) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT);
}
@Override
public void onFailure(Exception e) {
showTip(R.string.add_to_favorite_failure, LENGTH_LONG);
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof GalleryListScene;
}
}
private static class RemoveFromFavoriteListener extends EhCallback<GalleryListScene, Void> {
public RemoveFromFavoriteListener(Context context, int stageId, String sceneTag) {
super(context, stageId, sceneTag);
}
@Override
public void onSuccess(Void result) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT);
}
@Override
public void onFailure(Exception e) {
showTip(R.string.remove_from_favorite_failure, LENGTH_LONG);
}
@Override
public void onCancel() {
}
@Override
public boolean isInstance(SceneFragment scene) {
return scene instanceof GalleryListScene;
}
}
private static class HistoryHolder extends AbstractSwipeableItemViewHolder {
public final View card;
public final LoadImageView thumb;
public final TextView title;
public final TextView uploader;
public final SimpleRatingView rating;
public final TextView category;
public final TextView posted;
public final TextView simpleLanguage;
public final TextView pages;
public final ImageView downloaded;
public HistoryHolder(View itemView) {
super(itemView);
card = itemView.findViewById(R.id.card);
thumb = itemView.findViewById(R.id.thumb);
title = itemView.findViewById(R.id.title);
uploader = itemView.findViewById(R.id.uploader);
rating = itemView.findViewById(R.id.rating);
category = itemView.findViewById(R.id.category);
posted = itemView.findViewById(R.id.posted);
simpleLanguage = itemView.findViewById(R.id.simple_language);
pages = itemView.findViewById(R.id.pages);
downloaded = itemView.findViewById(R.id.downloaded);
}
@NonNull
@Override
public View getSwipeableContainerView() {
return card;
}
}
private class MoveDialogHelper implements DialogInterface.OnClickListener {
private final String[] mLabels;
private final GalleryInfo mGi;
public MoveDialogHelper(String[] labels, GalleryInfo gi) {
mLabels = labels;
mGi = gi;
}
@Override
public void onClick(DialogInterface dialog, int which) {
// Cancel check mode
Context context = getContext();
if (null == context) {
return;
}
if (null != mRecyclerView) {
mRecyclerView.outOfCustomChoiceMode();
}
DownloadManager downloadManager = EhApplication.getDownloadManager(context);
DownloadInfo downloadInfo = downloadManager.getDownloadInfo(mGi.gid);
if (downloadInfo == null) {
return;
}
String label = which == 0 ? null : mLabels[which];
downloadManager.changeLabel(Collections.singletonList(downloadInfo), label);
}
}
private class HistoryAdapter extends RecyclerView.Adapter<HistoryHolder>
implements SwipeableItemAdapter<HistoryHolder> {
private final LayoutInflater mInflater;
private final int mListThumbWidth;
private final int mListThumbHeight;
public HistoryAdapter() {
mInflater = getLayoutInflater();
@SuppressLint("InflateParams") View calculator = mInflater.inflate(R.layout.item_gallery_list_thumb_height, null);
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT);
mListThumbHeight = calculator.getMeasuredHeight();
mListThumbWidth = mListThumbHeight * 2 / 3;
}
@Override
public long getItemId(int position) {
if (null == mLazyList) {
return super.getItemId(position);
} else {
return mLazyList.get(position).gid;
}
}
@NonNull
@Override
public HistoryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
HistoryHolder holder = new HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false));
ViewGroup.LayoutParams lp = holder.thumb.getLayoutParams();
lp.width = mListThumbWidth;
lp.height = mListThumbHeight;
holder.thumb.setLayoutParams(lp);
return holder;
}
@Override
public void onBindViewHolder(@NonNull HistoryHolder holder, int position) {
if (null == mLazyList) {
return;
}
GalleryInfo gi = mLazyList.get(position);
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb);
holder.title.setText(EhUtils.getSuitableTitle(gi));
holder.uploader.setText(gi.uploader);
holder.rating.setRating(gi.rating);
TextView category = holder.category;
String newCategoryText = EhUtils.getCategory(gi.category);
if (!newCategoryText.contentEquals(category.getText())) {
category.setText(newCategoryText);
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category));
}
holder.posted.setText(gi.posted);
holder.pages.setText(null);
holder.pages.setVisibility(View.GONE);
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.setText(null);
holder.simpleLanguage.setVisibility(View.GONE);
} else {
holder.simpleLanguage.setText(gi.simpleLanguage);
holder.simpleLanguage.setVisibility(View.VISIBLE);
}
holder.downloaded.setVisibility(mDownloadManager.containDownloadInfo(gi.gid) ? View.VISIBLE : View.GONE);
// Update transition name
long gid = gi.gid;
ViewCompat.setTransitionName(holder.thumb, TransitionNameFactory.getThumbTransitionName(gid));
holder.card.setOnClickListener(v -> onItemClick(holder.itemView, position));
holder.card.setOnLongClickListener(v -> onItemLongClick(position));
}
@Override
public int getItemCount() {
return null != mLazyList ? mLazyList.size() : 0;
}
@Override
public int onGetSwipeReactionType(@NonNull HistoryHolder holder, int position, int x, int y) {
return SwipeableItemConstants.REACTION_CAN_SWIPE_LEFT;
}
@Override
public void onSwipeItemStarted(@NonNull HistoryHolder holder, int position) {
}
@Override
public void onSetSwipeBackground(@NonNull HistoryHolder holder, int position, int type) {
}
@Override
public SwipeResultAction onSwipeItem(@NonNull HistoryHolder holder, int position, int result) {
switch (result) {
case SwipeableItemConstants.RESULT_SWIPED_LEFT:
return new SwipeResultActionClear(position);
case SwipeableItemConstants.RESULT_SWIPED_RIGHT:
case SwipeableItemConstants.RESULT_CANCELED:
case SwipeableItemConstants.RESULT_NONE:
case SwipeableItemConstants.RESULT_SWIPED_DOWN:
case SwipeableItemConstants.RESULT_SWIPED_UP:
default:
return new SwipeResultActionDefault();
}
}
}
private class SwipeResultActionClear extends SwipeResultActionRemoveItem {
private final int mPosition;
protected SwipeResultActionClear(int position) {
mPosition = position;
}
@Override
protected void onPerformAction() {
super.onPerformAction();
if (null == mLazyList || null == mAdapter) {
return;
}
HistoryInfo info = mLazyList.get(mPosition);
EhDB.deleteHistoryInfo(info);
updateLazyList();
mAdapter.notifyDataSetChanged();
updateView(true);
}
}
}
@@ -1,489 +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.ui.scene
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingDataAdapter
import androidx.paging.cachedIn
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.easyrecyclerview.EasyRecyclerView
import com.hippo.easyrecyclerview.FastScroller
import com.hippo.easyrecyclerview.HandlerDrawable
import com.hippo.easyrecyclerview.MarginItemDecoration
import com.hippo.ehviewer.*
import com.hippo.ehviewer.client.EhCacheKeyFactory
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.HistoryInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener
import com.hippo.ehviewer.ui.CommonOperations
import com.hippo.ehviewer.ui.GalleryActivity
import com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter
import com.hippo.ehviewer.widget.SimpleRatingView
import com.hippo.scene.Announcer
import com.hippo.scene.SceneFragment
import com.hippo.view.ViewTransition
import com.hippo.widget.LoadImageView
import com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager
import com.hippo.yorozuya.ViewUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
class HistoryScene : ToolbarScene() {
private var mRecyclerView: EasyRecyclerView? = null
private var mViewTransition: ViewTransition? = null
private var mAdapter: HistoryAdapter? = null
private var mDownloadManager: DownloadManager? = null
private var mDownloadInfoListener: DownloadInfoListener? = null
private var mFavouriteStatusRouter: FavouriteStatusRouter? = null
private var mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener? = null
override fun onDestroy() {
super.onDestroy()
mDownloadManager!!.removeDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouter!!.removeListener(mFavouriteStatusRouterListener)
}
override fun getNavCheckedItem(): Int {
return R.id.nav_history
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = requireContext()
mDownloadManager = EhApplication.getDownloadManager(context)
mFavouriteStatusRouter = EhApplication.getFavouriteStatusRouter(context)
mDownloadInfoListener = object : DownloadInfoListener {
override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter?.notifyDataSetChanged()
}
override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}
override fun onUpdateAll() {}
override fun onReload() {
mAdapter?.notifyDataSetChanged()
}
override fun onChange() {
mAdapter?.notifyDataSetChanged()
}
override fun onRenameLabel(from: String, to: String) {}
override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {
mAdapter?.notifyDataSetChanged()
}
override fun onUpdateLabels() {}
}
mDownloadManager!!.addDownloadInfoListener(mDownloadInfoListener)
mFavouriteStatusRouterListener = FavouriteStatusRouter.Listener { _: Long, _: Int ->
mAdapter?.notifyDataSetChanged()
}
mFavouriteStatusRouter!!.addListener(mFavouriteStatusRouterListener)
}
override fun onCreateViewWithToolbar(
inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.scene_history, container, false)
val content = ViewUtils.`$$`(view, R.id.content)
val recyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as EasyRecyclerView
val mFastScroller = ViewUtils.`$$`(content, R.id.fast_scroller) as FastScroller
val mTip = ViewUtils.`$$`(view, R.id.tip) as TextView
mViewTransition = ViewTransition(content, mTip)
val resources = requireContext().resources
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_history)
drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
mTip.setCompoundDrawables(null, drawable, null, null)
val historyData = Pager(
PagingConfig(20)
) {
EhDB.getHistoryLazyList()
}.flow.cachedIn(viewLifecycleOwner.lifecycleScope)
mAdapter = HistoryAdapter(object : DiffUtil.ItemCallback<HistoryInfo>() {
override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean {
return oldItem.gid == newItem.gid
}
})
recyclerView.adapter = mAdapter
val layoutManager = AutoStaggeredGridLayoutManager(
0, StaggeredGridLayoutManager.VERTICAL
)
layoutManager.setColumnSize(resources.getDimensionPixelOffset(Settings.getDetailSizeResId()))
layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)
recyclerView.layoutManager = layoutManager
recyclerView.clipToPadding = false
recyclerView.clipChildren = false
val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)
val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)
val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)
val decoration = MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)
recyclerView.addItemDecoration(decoration)
val itemTouchHelper = ItemTouchHelper(HistoryItemTouchHelperCallback())
itemTouchHelper.attachToRecyclerView(recyclerView)
mFastScroller.attachToRecyclerView(recyclerView)
mRecyclerView = recyclerView
val handlerDrawable = HandlerDrawable()
handlerDrawable.setColor(theme.resolveColor(com.google.android.material.R.attr.colorPrimary))
mFastScroller.setHandlerDrawable(handlerDrawable)
viewLifecycleOwner.lifecycleScope.launch {
historyData.collectLatest { value ->
mAdapter!!.submitData(
value
)
}
}
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(R.string.history)
setNavigationIcon(R.drawable.ic_baseline_menu_24)
}
override fun onDestroyView() {
super.onDestroyView()
mRecyclerView?.stopScroll()
mRecyclerView = null
mViewTransition = null
mAdapter = null
}
@SuppressLint("RtlHardcoded")
override fun onNavigationClick() {
toggleDrawer(Gravity.LEFT)
}
override fun getMenuResId(): Int {
return R.menu.scene_history
}
@SuppressLint("NotifyDataSetChanged")
private fun showClearAllDialog() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.clear_all_history)
.setPositiveButton(R.string.clear_all) { _: DialogInterface?, which: Int ->
if (DialogInterface.BUTTON_POSITIVE != which || null == mAdapter) {
return@setPositiveButton
}
EhDB.clearHistoryInfo()
mAdapter!!.refresh()
mAdapter!!.notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
// Skip when in choice mode
context ?: return false
val id = item.itemId
if (id == R.id.action_clear_all) {
showClearAllDialog()
return true
}
return false
}
fun onItemClick(position: Int): Boolean {
val args = Bundle()
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mAdapter?.peek(position))
val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)
startScene(announcer)
return true
}
fun onItemLongClick(position: Int): Boolean {
val context = requireContext()
val activity = mainActivity ?: return false
val gi: GalleryInfo = mAdapter?.peek(position) ?: return true
val downloaded = mDownloadManager!!.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID
val favourited = gi.favoriteSlot != -2
val items = if (downloaded) arrayOf<CharSequence>(
context.getString(R.string.read),
context.getString(R.string.delete_downloads),
context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),
context.getString(R.string.download_move_dialog_title)
) else arrayOf<CharSequence>(
context.getString(R.string.read),
context.getString(R.string.download),
context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),
)
val icons = if (downloaded) intArrayOf(
R.drawable.v_book_open_x24,
R.drawable.v_delete_x24,
if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,
R.drawable.v_folder_move_x24
) else intArrayOf(
R.drawable.v_book_open_x24,
R.drawable.v_download_x24,
if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,
)
MaterialAlertDialogBuilder(context)
.setTitle(EhUtils.getSuitableTitle(gi))
.setAdapter(
SelectItemWithIconAdapter(
context,
items,
icons
)
) { _: DialogInterface?, which: Int ->
when (which) {
0 -> {
val intent = Intent(activity, GalleryActivity::class.java)
intent.action = GalleryActivity.ACTION_EH
intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi)
startActivity(intent)
}
1 -> if (downloaded) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_remove_dialog_title)
.setMessage(
getString(
R.string.download_remove_dialog_message,
gi.title
)
)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
mDownloadManager!!.deleteDownload(
gi.gid
)
}
.show()
} else {
CommonOperations.startDownload(activity, gi, false)
}
2 -> if (favourited) {
CommonOperations.removeFromFavorites(
activity,
gi,
RemoveFromFavoriteListener(context, activity.stageId, tag)
)
} else {
CommonOperations.addToFavorites(
activity,
gi,
AddToFavoriteListener(context, activity.stageId, tag)
)
}
3 -> {
val labelRawList = EhApplication.getDownloadManager(context).labelList
val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)
labelList.add(getString(R.string.default_download_label_name))
var i = 0
val n = labelRawList.size
while (i < n) {
labelList.add(labelRawList[i].label)
i++
}
val labels = labelList.toTypedArray()
val helper = MoveDialogHelper(labels, gi)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.download_move_dialog_title)
.setItems(labels, helper)
.show()
}
}
}.show()
return true
}
private class AddToFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
override fun onSuccess(result: Void?) {
showTip(R.string.add_to_favorite_success, LENGTH_SHORT)
}
override fun onFailure(e: Exception) {
showTip(R.string.add_to_favorite_failure, LENGTH_LONG)
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class RemoveFromFavoriteListener(context: Context?, stageId: Int, sceneTag: String?) :
EhCallback<GalleryListScene?, Void?>(context, stageId, sceneTag) {
override fun onSuccess(result: Void?) {
showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)
}
override fun onFailure(e: Exception) {
showTip(R.string.remove_from_favorite_failure, LENGTH_LONG)
}
override fun onCancel() {}
override fun isInstance(scene: SceneFragment): Boolean {
return scene is GalleryListScene
}
}
private class HistoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val card: View
val thumb: LoadImageView
val title: TextView
val uploader: TextView
val rating: SimpleRatingView
val category: TextView
val posted: TextView
val simpleLanguage: TextView
val pages: TextView
val downloaded: ImageView
init {
card = itemView.findViewById(R.id.card)
thumb = itemView.findViewById(R.id.thumb)
title = itemView.findViewById(R.id.title)
uploader = itemView.findViewById(R.id.uploader)
rating = itemView.findViewById(R.id.rating)
category = itemView.findViewById(R.id.category)
posted = itemView.findViewById(R.id.posted)
simpleLanguage = itemView.findViewById(R.id.simple_language)
pages = itemView.findViewById(R.id.pages)
downloaded = itemView.findViewById(R.id.downloaded)
}
}
private inner class MoveDialogHelper(
private val mLabels: Array<String>,
private val mGi: GalleryInfo
) : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface, which: Int) {
mRecyclerView?.outOfCustomChoiceMode()
val downloadManager = EhApplication.getDownloadManager(requireContext())
val downloadInfo = downloadManager.getDownloadInfo(mGi.gid) ?: return
val label = if (which == 0) null else mLabels[which]
downloadManager.changeLabel(listOf(downloadInfo), label)
}
}
private inner class HistoryAdapter(diffCallback: DiffUtil.ItemCallback<HistoryInfo>) :
PagingDataAdapter<HistoryInfo, HistoryHolder>(diffCallback) {
private val mInflater: LayoutInflater = layoutInflater
private val mListThumbWidth: Int
private val mListThumbHeight: Int
init {
@SuppressLint("InflateParams") val calculator =
mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)
ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)
mListThumbHeight = calculator.measuredHeight
mListThumbWidth = mListThumbHeight * 2 / 3
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryHolder {
val holder = HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false))
val lp = holder.thumb.layoutParams
lp.width = mListThumbWidth
lp.height = mListThumbHeight
holder.thumb.layoutParams = lp
return holder
}
override fun onBindViewHolder(holder: HistoryHolder, position: Int) {
val gi: GalleryInfo? = getItem(position)
gi ?: return
holder.thumb.load(EhCacheKeyFactory.getThumbKey(gi.gid), gi.thumb)
holder.title.text = EhUtils.getSuitableTitle(gi)
holder.uploader.text = gi.uploader
holder.rating.rating = gi.rating
val category = holder.category
val newCategoryText = EhUtils.getCategory(gi.category)
if (!newCategoryText!!.contentEquals(category.text)) {
category.text = newCategoryText
category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))
}
holder.posted.text = gi.posted
holder.pages.text = null
holder.pages.visibility = View.GONE
if (TextUtils.isEmpty(gi.simpleLanguage)) {
holder.simpleLanguage.text = null
holder.simpleLanguage.visibility = View.GONE
} else {
holder.simpleLanguage.text = gi.simpleLanguage
holder.simpleLanguage.visibility = View.VISIBLE
}
holder.downloaded.visibility =
if (mDownloadManager!!.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE
// Update transition name
val gid = gi.gid
ViewCompat.setTransitionName(
holder.thumb,
TransitionNameFactory.getThumbTransitionName(gid)
)
holder.card.setOnClickListener { onItemClick(position) }
holder.card.setOnLongClickListener { onItemLongClick(position) }
}
}
private inner class HistoryItemTouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(0, ItemTouchHelper.LEFT)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val mPosition = viewHolder.bindingAdapterPosition
if (mAdapter == null) {
return
}
val info: HistoryInfo = mAdapter!!.peek(mPosition)!!
EhDB.deleteHistoryInfo(info)
mAdapter!!.refresh()
}
}
}
@@ -16,11 +16,10 @@
* If not, see <https://www.gnu.org/licenses/>.
*/
package com.hippo.ehviewer.ui
package com.hippo.ehviewer.ui.scene
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
@@ -31,10 +30,14 @@ import androidx.fragment.app.FragmentActivity
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
class SecurityActivity : AppCompatActivity() {
class SecurityScene : SolidScene() {
override fun needShowLeftDrawer(): Boolean {
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isAuthenticationSupported(this))
if (isAuthenticationSupported(requireContext()))
startAuthentication(getString(R.string.settings_privacy_require_unlock))
else
onSuccess()
@@ -49,7 +52,7 @@ class SecurityActivity : AppCompatActivity() {
title = title,
subtitle = subtitle,
confirmationRequired = confirmationRequired,
executor = ContextCompat.getMainExecutor(this),
executor = ContextCompat.getMainExecutor(requireContext()),
callback = AuthenticationCallback()
)
}
@@ -67,7 +70,7 @@ class SecurityActivity : AppCompatActivity() {
errorCode: Int,
errString: CharSequence
) {
moveTaskToBack(true)
requireActivity().finish()
}
}
@@ -81,6 +84,8 @@ class SecurityActivity : AppCompatActivity() {
private fun onSuccess() {
EhApplication.locked = false
if (stackIndex == 0)
startSceneForCheckStep(CHECK_STEP_SECURITY, arguments)
finish()
}
}
@@ -20,6 +20,7 @@ import android.os.Bundle;
import android.util.Log;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.client.EhUtils;
import com.hippo.scene.Announcer;
/**
@@ -27,14 +28,26 @@ import com.hippo.scene.Announcer;
*/
public class SolidScene extends BaseScene {
public static final int CHECK_STEP_SIGN_IN = 1;
public static final int CHECK_STEP_SELECT_SITE = 2;
public static final int CHECK_STEP_SECURITY = 0;
public static final int CHECK_STEP_WARNING = 1;
public static final int CHECK_STEP_SIGN_IN = 2;
public static final int CHECK_STEP_SELECT_SITE = 3;
public static final String KEY_TARGET_SCENE = "target_scene";
public static final String KEY_TARGET_ARGS = "target_args";
private static final String TAG = SolidScene.class.getSimpleName();
public void startSceneForCheckStep(int checkStep, Bundle args) {
switch (checkStep) {
case CHECK_STEP_SECURITY:
if (Settings.getShowWarning()) {
startScene(new Announcer(WarningScene.class).setArgs(args), true);
break;
}
case CHECK_STEP_WARNING:
if (EhUtils.needSignedIn(getContext())) {
startScene(new Announcer(SignInScene.class).setArgs(args), true);
break;
}
case CHECK_STEP_SIGN_IN:
if (Settings.getSelectSite()) {
startScene(new Announcer(SelectSiteScene.class).setArgs(args), true);
@@ -87,6 +87,12 @@ public abstract class ToolbarScene extends BaseScene {
}
}
@Override
public boolean needWhiteStatusBar() {
return false;
}
public int getMenuResId() {
return 0;
}
@@ -0,0 +1,85 @@
/*
* 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.ui.scene;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hippo.ehviewer.R;
import com.hippo.ehviewer.Settings;
import com.hippo.ehviewer.ui.MainActivity;
import com.hippo.ehviewer.ui.annotation.ViewLifeCircle;
import com.hippo.yorozuya.ViewUtils;
public final class WarningScene extends SolidScene implements View.OnClickListener {
@Nullable
private View mReject;
@Nullable
@ViewLifeCircle
private View mAccept;
@Override
public boolean needShowLeftDrawer() {
return false;
}
@NonNull
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.scene_warning, container, false);
mReject = ViewUtils.$$(view, R.id.reject);
mAccept = ViewUtils.$$(view, R.id.accept);
mReject.setOnClickListener(this);
mAccept.setOnClickListener(this);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mReject = null;
mAccept = null;
}
@Override
public void onClick(View v) {
if (mReject == v) {
finishStage();
} else if (mAccept == v) {
// Never show this warning anymore
Settings.putShowWarning(false);
// Start new scene and finish it self
MainActivity activity = getMainActivity();
if (null != activity) {
startSceneForCheckStep(CHECK_STEP_WARNING, getArguments());
}
finish();
}
}
}
@@ -53,6 +53,11 @@ public class FixedThumb extends LoadImageView {
a.recycle();
}
public void setFix(float minAspect, float maxAspect) {
this.minAspect = minAspect;
this.maxAspect = maxAspect;
}
@Override
public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) {
if (alwaysCutAndScale) {
@@ -1,13 +0,0 @@
package com.hippo.ehviewer.widget
import android.content.Context
import android.util.AttributeSet
import com.hippo.ehviewer.Settings
class ResizeableFixedThumb @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FixedThumb(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(Settings.getListThumbSize() * 2, Settings.getListThumbSize() * 3)
}
}
@@ -242,7 +242,7 @@ class SearchLayout @JvmOverloads constructor(
override fun onTabReselected(tab: TabLayout.Tab) {}
@IntDef(SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE)
@Retention(AnnotationRetention.SOURCE)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
private annotation class SearchMode
interface Helper {
fun onChangeSearchMode()
@@ -57,7 +57,6 @@ public abstract class GalleryProvider {
public void stop() {
OSUtils.checkMainLoop();
mImageCache.evictAll();
Image.lazyEvictAll();
}
public void setGLRoot(GLRoot glRoot) {
@@ -246,14 +245,18 @@ public abstract class GalleryProvider {
}
public void add(Integer key, ImageWrapper value) {
if (value.getFormat() != Image.FORMAT_ANIMATED)
if (value.obtain())
put(key, value);
if (value.getFormat() != Image.FORMAT_GIF && value.obtain()) {
put(key, value);
}
}
@Override
protected int sizeOf(Integer key, ImageWrapper value) {
return value.getWidth() * value.getHeight() * 4;
int size = value.getWidth() * value.getHeight() * 4;
if (value.getFormat() == Image.FORMAT_GIF) {
size *= 5;
}
return size;
}
@Override
@@ -983,13 +983,15 @@ public final class GalleryView extends GLView implements GestureRecognizer.Liste
private final Context mContext;
private final Adapter mAdapter;
private final int mEdgeColor = Color.WHITE;
private Listener mListener;
private int mLayoutMode = LAYOUT_LEFT_TO_RIGHT;
private int mScaleMode = SCALE_FIT;
private int mStartPosition = START_POSITION_TOP_LEFT;
private int mStartPage = 0;
private int mBackgroundColor = Color.BLACK;
private int mEdgeColor = Color.WHITE;
private int mPagerInterval = 48;
private int mScrollInterval = 24;
private int mPageMinHeight = 256;
@@ -62,8 +62,8 @@ public class GLES11Canvas implements GLCanvas {
private static final int MSKEW_Y = 1;
private static final int MSCALE_X = 0;
private static final int MSCALE_Y = 5;
private static final float[] sCropRect = new float[4];
private static final GLId mGLId = new GLES11IdImpl();
private static float[] sCropRect = new float[4];
private static GLId mGLId = new GLES11IdImpl();
static {
float[] temp = {
@@ -98,12 +98,12 @@ public class GLES11Canvas implements GLCanvas {
}
}
private final float[] mMatrixValues = new float[16];
private final float[] mTextureMatrixValues = new float[16];
private final float mMatrixValues[] = new float[16];
private final float mTextureMatrixValues[] = new float[16];
// The results of mapPoints are stored in this buffer, and the order is
// x1, y1, x2, y2.
private final float[] mMapPointsBuffer = new float[4];
private final float[] mTextureColor = new float[4];
private final float mMapPointsBuffer[] = new float[4];
private final float mTextureColor[] = new float[4];
private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
private final RectF mDrawTextureSourceRect = new RectF();
@@ -111,21 +111,21 @@ public class GLES11Canvas implements GLCanvas {
private final float[] mTempMatrix = new float[32];
private final IntList mUnboundTextures = new IntList();
private final IntList mDeleteBuffers = new IntList();
private final GL11 mGL;
private final int mBoxCoords;
private final GLState mGLState;
private final boolean mBlendEnabled = true;
private final int[] mFrameBuffer = new int[1];
// Drawing statistics
int mCountDrawLine;
int mCountFillRect;
int mCountDrawMesh;
int mCountTextureRect;
int mCountTextureOES;
private GL11 mGL;
private int mBoxCoords;
private GLState mGLState;
private float mAlpha;
private ConfigState mRecycledRestoreAction;
private int mScreenWidth;
private int mScreenHeight;
private boolean mBlendEnabled = true;
private int mFrameBuffer[] = new int[1];
private RawTexture mTargetTexture;
public GLES11Canvas(GL11 gl) {
@@ -191,7 +191,7 @@ public class GLES11Canvas implements GLCanvas {
}
}
private static boolean isMatrixRotatedOrFlipped(float[] matrix) {
private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
final float eps = 1e-5f;
return Math.abs(matrix[MSKEW_X]) > eps
|| Math.abs(matrix[MSKEW_Y]) > eps
@@ -247,7 +247,7 @@ public class GLES11Canvas implements GLCanvas {
gl.glMatrixMode(GL11.GL_MODELVIEW);
gl.glLoadIdentity();
float[] matrix = mMatrixValues;
float matrix[] = mMatrixValues;
Matrix.setIdentityM(matrix, 0);
// to match the graphic coordinate system in android, we flip it vertically.
if (mTargetTexture == null) {
@@ -408,7 +408,7 @@ public class GLES11Canvas implements GLCanvas {
}
@Override
public void multiplyMatrix(float[] matrix, int offset) {
public void multiplyMatrix(float matrix[], int offset) {
float[] temp = mTempMatrix;
Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
System.arraycopy(temp, 0, mMatrixValues, 0, 16);
@@ -467,7 +467,7 @@ public class GLES11Canvas implements GLCanvas {
// Transforms two points by the given matrix m. The result
// {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
private float[] mapPoints(float[] m, int x1, int y1, int x2, int y2) {
private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
float[] r = mMapPointsBuffer;
// Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
@@ -506,7 +506,7 @@ public class GLES11Canvas implements GLCanvas {
textureRect(x, y, width, height);
} else {
// draw the rect from bottom-left to top-right
float[] points = mapPoints(
float points[] = mapPoints(
mMatrixValues, x, y + height, x + width, y);
x = (int) (points[0] + 0.5f);
y = (int) (points[1] + 0.5f);
@@ -958,12 +958,13 @@ public class GLES11Canvas implements GLCanvas {
private static class GLState {
private final GL11 mGL;
private final boolean mLineSmooth = false;
private int mTexEnvMode = GL11.GL_REPLACE;
private float mTextureAlpha = 1.0f;
private int mTextureTarget = GL11.GL_TEXTURE_2D;
private boolean mBlendEnabled = true;
private float mLineWidth = 1.0f;
private boolean mLineSmooth = false;
public GLState(GL11 gl) {
mGL = gl;
@@ -1058,7 +1059,7 @@ public class GLES11Canvas implements GLCanvas {
private static class ConfigState {
float mAlpha;
float[] mMatrix = new float[16];
float mMatrix[] = new float[16];
ConfigState mNextFree;
public void restore(GLES11Canvas canvas) {
@@ -30,9 +30,9 @@ class NinePatchChunk {
public Rect mPaddings = new Rect();
public int[] mDivX;
public int[] mDivY;
public int[] mColor;
public int mDivX[];
public int mDivY[];
public int mColor[];
private static void readIntArray(int[] data, ByteBuffer buffer) {
for (int i = 0, n = data.length; i < n; ++i) {
@@ -35,9 +35,9 @@ import java.nio.FloatBuffer;
public class NinePatchTexture extends ResourceTexture {
@SuppressWarnings("unused")
private static final String TAG = "NinePatchTexture";
private final SmallCache<NinePatchInstance> mInstanceCache
= new SmallCache<NinePatchInstance>();
private NinePatchChunk mChunk;
private SmallCache<NinePatchInstance> mInstanceCache
= new SmallCache<NinePatchInstance>();
public NinePatchTexture(Context context, int resId) {
super(context, resId);
@@ -120,8 +120,8 @@ public class NinePatchTexture extends ResourceTexture {
private static class SmallCache<V> {
private static final int CACHE_SIZE = 16;
private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
private final int[] mKey = new int[CACHE_SIZE];
private final V[] mValue = (V[]) new Object[CACHE_SIZE];
private int[] mKey = new int[CACHE_SIZE];
private V[] mValue = (V[]) new Object[CACHE_SIZE];
private int mCount; // number of items in this cache
// Puts a value into the cache. If the cache is full, also returns
@@ -217,10 +217,10 @@ class NinePatchInstance {
throw new RuntimeException("unsupported nine patch");
}
float[] divX = new float[4];
float[] divY = new float[4];
float[] divU = new float[4];
float[] divV = new float[4];
float divX[] = new float[4];
float divY[] = new float[4];
float divU[] = new float[4];
float divV[] = new float[4];
int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
@@ -261,7 +261,7 @@ class NinePatchInstance {
* @return the number of these dividers.
*/
private static int stretch(
float[] x, float[] u, int[] div, int source, int target) {
float x[], float u[], int div[], int source, int target) {
int textureSize = MathUtils.nextPowerOf2(source);
float textureBound = (float) source / textureSize;
@@ -313,7 +313,7 @@ class NinePatchInstance {
return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
}
private void prepareVertexData(float[] x, float[] y, float[] u, float[] v,
private void prepareVertexData(float x[], float y[], float u[], float v[],
int nx, int ny, int[] color) {
/*
* Given a 3x3 nine-patch image, the vertex order is defined as the
@@ -335,8 +335,8 @@ class NinePatchInstance {
* index: 04152637B6A5948C9DAEBF
*/
int pntCount = 0;
float[] xy = new float[VERTEX_BUFFER_SIZE];
float[] uv = new float[VERTEX_BUFFER_SIZE];
float xy[] = new float[VERTEX_BUFFER_SIZE];
float uv[] = new float[VERTEX_BUFFER_SIZE];
for (int j = 0; j < ny; ++j) {
for (int i = 0; i < nx; ++i) {
int xIndex = (pntCount++) << 1;
@@ -350,7 +350,7 @@ class NinePatchInstance {
int idxCount = 1;
boolean isForward = false;
byte[] index = new byte[INDEX_BUFFER_SIZE];
byte index[] = new byte[INDEX_BUFFER_SIZE];
for (int row = 0; row < ny - 1; row++) {
--idxCount;
isForward = !isForward;
@@ -153,6 +153,12 @@ public class ImageTexture implements Texture, Animatable {
}
mTiles = list.toArray(new Tile[list.size()]);
if (!mImage.isCompleted()) {
Runnable runnable = new AnimateRunnable();
mAnimateRunnable = runnable;
sThreadExecutor.execute(runnable);
}
}
private static Tile obtainSmallTile() {
@@ -233,7 +239,7 @@ public class ImageTexture implements Texture, Animatable {
}
boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() ||
(mImage.getFormat() == 0) || mRunning.get();
(mImage.isCompleted() && mImage.getFrameCount() <= 1) || mRunning.get();
synchronized (mImage) {
mImageBusy = false;
@@ -591,11 +597,25 @@ public class ImageTexture implements Texture, Animatable {
mImageBusy = true;
}
if (!mImage.isCompleted()) {
try {
sPVLock.p();
} catch (InterruptedException e) {
// Ignore
}
if (!mNeedRelease.get()) {
mImage.complete();
}
sPVLock.v();
}
int frameCount = mImage.getFrameCount();
synchronized (mImage) {
// Release image
mImageBusy = false;
// Check need release, frameCount <= 1
if (mNeedRelease.get() || mImage.getFormat() == 0) {
if (mNeedRelease.get() || frameCount <= 1) {
mAnimateRunnable = null;
return;
}
@@ -122,6 +122,22 @@ public class ImageWrapper {
return mCut.height();
}
/**
* @see Image#complete()
*/
public boolean complete() {
synchronized (mImage) {
return mImage.complete();
}
}
/**
* @see Image#isCompleted()
*/
public boolean isCompleted() {
return mImage.isCompleted();
}
/**
* @see Image#render(int, int, Bitmap, int, int, int, int, boolean, int)
*/
@@ -152,6 +168,13 @@ public class ImageWrapper {
return mImage.getDelay();
}
/**
* @see Image#getFrameCount()
*/
public int getFrameCount() {
return mImage.getFrameCount();
}
/**
* @see Image#isOpaque()
*/
@@ -54,17 +54,21 @@ public class GLProgressView extends GLView {
}
private final GLPaint mGLPaint;
private final List<Animation> mAnimations;
private float mCx;
private float mCy;
private float mRadiusX;
private float mRadiusY;
private float mTrimStart = 0.0f;
private float mTrimEnd = 0.0f;
private float mTrimOffset = 0.0f;
private float mTrimRotation = 0.0f;
private boolean mIndeterminate = false;
private List<Animation> mAnimations;
public GLProgressView() {
mGLPaint = new GLPaint();
mGLPaint.setColor(Color.WHITE);
@@ -1,246 +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.image;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import java.io.FileDescriptor;
import java.util.ArrayList;
/**
* The {@code Image} is a image which stored pixel data in native heap
*/
public final class Image {
public static final int FORMAT_NORMAL = 0;
public static final int FORMAT_ANIMATED = 1;
private static final ArrayList<Image> imageList = new ArrayList<>();
private static boolean needEvictAll = false;
private long mNativePtr;
private boolean needRecycle = false;
private Integer nativeProcessCnt = 0;
private Image(long nativePtr) {
if (needEvictAll)
evictAll();
mNativePtr = nativePtr;
imageList.add(this);
}
private static Image newFromAddr(long addr) {
if (addr == 0)
return null;
return new Image(addr);
}
public static void lazyEvictAll() {
needEvictAll = true;
}
private static void evictAll() {
(new ArrayList<>(imageList)).forEach(Image::recycle);
needEvictAll = false;
}
/**
* Decode image from {@code InputStream}
*/
@Nullable
public static Image decode(FileDescriptor fd, boolean partially) {
return newFromAddr(nativeDecode(fd));
}
/**
* Decode image from {@code InputStream}
*/
@Nullable
public static Image decode(Integer fd, boolean partially) {
return newFromAddr(nativeDecodeFdInt(fd));
}
/**
* Decode image from {@code InputStream}
*/
@Nullable
public static Image decodeAddr(Long addr, boolean partially) {
return newFromAddr(nativeDecodeAddr(addr));
}
/**
* Create a plain image from Bitmap
*/
@Nullable
public static Image create(Bitmap bitmap) {
return newFromAddr(nativeCreate(bitmap));
}
/**
* Return all un-recycled {@code Image} instance count.
* It is useful for debug.
*/
public static int getImageCount() {
return imageList.size();
}
private static native long nativeDecode(FileDescriptor fd);
private static native long nativeDecodeFdInt(int fd);
private static native long nativeDecodeAddr(long addr);
private static native long nativeCreate(Bitmap bitmap);
private static native void nativeRender(long nativePtr,
int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height);
private static native void nativeTexImage(long nativePtr, boolean init, int offsetX, int offsetY, int width, int height);
private static native void nativeAdvance(long nativePtr);
private static native int nativeGetDelay(long nativePtr);
private static native boolean nativeIsOpaque(long nativePtr);
private static native void nativeRecycle(long nativePtr);
private static native int nativeGetFormat(long nativePtr);
private static native int nativeGetWidth(long nativePtr);
private static native int nativeGetHeight(long nativePtr);
/**
* Return the format of the image
*/
public int getFormat() {
checkRecycledAndLock();
int ret = nativeGetFormat(mNativePtr);
releaseNativeLock();
return ret;
}
/**
* Return the width of the image
*/
public int getWidth() {
checkRecycledAndLock();
int ret = nativeGetWidth(mNativePtr);
releaseNativeLock();
return ret;
}
/**
* Return the height of the image
*/
public int getHeight() {
checkRecycledAndLock();
int ret = nativeGetHeight(mNativePtr);
releaseNativeLock();
return ret;
}
private synchronized void checkRecycledAndLock() {
if (mNativePtr == 0) {
throw new IllegalStateException("The image is recycled.");
}
nativeProcessCnt++;
}
private synchronized void releaseNativeLock() {
nativeProcessCnt--;
if (needRecycle)
recycle();
}
/**
* Render the image to {@code Bitmap}
*/
public void render(int srcX, int srcY, Bitmap dst, int dstX, int dstY,
int width, int height, boolean fillBlank, int defaultColor) {
checkRecycledAndLock();
nativeRender(mNativePtr, srcX, srcY, dst, dstX, dstY, width, height);
releaseNativeLock();
}
/**
* Call {@code glTexImage2D} for init is true and
* call {@code glTexSubImage2D} for init is false.
* width * height must <= 512 * 512 or do nothing
*/
public void texImage(boolean init, int offsetX, int offsetY, int width, int height) {
checkRecycledAndLock();
nativeTexImage(mNativePtr, init, offsetX, offsetY, width, height);
releaseNativeLock();
}
/**
* Move to next frame. Do nothing for non-animation image
*/
public void advance() {
checkRecycledAndLock();
nativeAdvance(mNativePtr);
releaseNativeLock();
}
/**
* Return current frame delay. 0 for non-animation image
*/
public int getDelay() {
checkRecycledAndLock();
int delay = nativeGetDelay(mNativePtr);
releaseNativeLock();
return delay <= 10 ? 100 : delay;
}
/**
* Return is the image opaque
*/
public boolean isOpaque() {
checkRecycledAndLock();
boolean ret = nativeIsOpaque(mNativePtr);
releaseNativeLock();
return ret;
}
/**
* Free the native object associated with this image.
* It must be called when the image will not be used.
* The image can't be used after this method is called.
*/
public synchronized void recycle() {
if (mNativePtr != 0) {
if (nativeProcessCnt != 0) {
needRecycle = true;
return;
}
nativeRecycle(mNativePtr);
mNativePtr = 0;
imageList.remove(this);
}
}
/**
* Returns true if this image has been recycled.
*/
public boolean isRecycled() {
return mNativePtr == 0;
}
}
@@ -104,13 +104,17 @@ final class DocumentsContractApi19 {
return true;
}
// Writable normal files considered writable
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type)
&& (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) {
// Directories that allow create considered writable
return true;
} else return !TextUtils.isEmpty(type)
&& (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0;
} else if (!TextUtils.isEmpty(type)
&& (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) {
// Writable normal files considered writable
return true;
}
return false;
}
public static boolean delete(Context context, Uri self) {
@@ -33,7 +33,6 @@ final class DocumentsContractApi21 {
private static final String TAG = DocumentsContractApi21.class.getSimpleName();
private static final String PATH_DOCUMENT = "document";
private static final String PATH_TREE = "tree";
private DocumentsContractApi21() {
}
@@ -41,7 +41,6 @@ class FileDescriptorRandomAccessFile implements UniRandomAccessFile {
private final int flushAfterWrite;
private final Object closeLock = new Object();
private volatile boolean closed = false;
private FileDescriptorRandomAccessFile(ParcelFileDescriptor pfd, FileDescriptor fd, String mode) {
this.pfd = pfd;
this.fd = fd;
@@ -25,7 +25,7 @@ import java.io.RandomAccessFile;
class RawRandomAccessFile implements UniRandomAccessFile {
private final RandomAccessFile mFile;
private RandomAccessFile mFile;
RawRandomAccessFile(RandomAccessFile file) {
mFile = file;
@@ -38,7 +38,7 @@ import java.io.OutputStream;
// TODO need more test
class UriOutputStream extends FileOutputStream {
private final ParcelFileDescriptor mPfd;
private ParcelFileDescriptor mPfd;
private UriOutputStream(ParcelFileDescriptor pfd, FileDescriptor fd) {
super(fd);
@@ -82,20 +82,6 @@ public class BatteryView extends AppCompatTextView {
}
mCurrentColor = color;
super.setTextColor(color);
}
private void startCharger() {
if (!mIsChargerWorking) {
getHandler().post(mCharger);
mIsChargerWorking = true;
}
}
private void stopCharger() {
if (mIsChargerWorking) {
getHandler().removeCallbacks(mCharger);
mIsChargerWorking = false;
}
} private final Runnable mCharger = new Runnable() {
private int level = 0;
@@ -111,6 +97,20 @@ public class BatteryView extends AppCompatTextView {
}
};
private void startCharger() {
if (!mIsChargerWorking) {
getHandler().post(mCharger);
mIsChargerWorking = true;
}
}
private void stopCharger() {
if (mIsChargerWorking) {
getHandler().removeCallbacks(mCharger);
mIsChargerWorking = false;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -145,7 +145,6 @@ public class BatteryView extends AppCompatTextView {
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
@@ -97,17 +97,6 @@ public class TextClock extends AppCompatTextView {
} else {
mTime = Calendar.getInstance();
}
}
public CharSequence getFormat12Hour() {
return mFormat12;
}
public void setFormat12Hour(CharSequence format) {
mFormat12 = format;
chooseFormat();
onTimeChanged();
} private final Runnable mTicker = new Runnable() {
@Override
public void run() {
@@ -120,6 +109,17 @@ public class TextClock extends AppCompatTextView {
}
};
public CharSequence getFormat12Hour() {
return mFormat12;
}
public void setFormat12Hour(CharSequence format) {
mFormat12 = format;
chooseFormat();
onTimeChanged();
}
public CharSequence getFormat24Hour() {
return mFormat24;
}
@@ -144,7 +144,19 @@ public class TextClock extends AppCompatTextView {
createTime(timeZone);
onTimeChanged();
}
} private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
chooseFormat();
onTimeChanged();
}
@Override
public void onChange(boolean selfChange, Uri uri) {
chooseFormat();
onTimeChanged();
}
};
/**
* Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
@@ -203,19 +215,7 @@ public class TextClock extends AppCompatTextView {
onTimeChanged();
}
}
} private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
chooseFormat();
onTimeChanged();
}
@Override
public void onChange(boolean selfChange, Uri uri) {
chooseFormat();
onTimeChanged();
}
};
}
@Override
protected void onDetachedFromWindow() {
@@ -27,7 +27,6 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public final class FileUtils {
@@ -155,7 +154,7 @@ public final class FileUtils {
OutputStream os = null;
try {
os = new FileOutputStream(file);
os.write(str.getBytes(StandardCharsets.UTF_8));
os.write(str.getBytes("utf-8"));
return true;
} catch (IOException e) {
e.printStackTrace();
@@ -20,7 +20,7 @@ import android.os.Process;
public class PriorityThread extends Thread {
private final int mPriority;
private int mPriority;
public PriorityThread(Runnable runnable, int priority) {
super(runnable);
@@ -28,9 +28,9 @@ 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;
private long mKeepAliveMillis;
private Queue<Runnable> mWorkQueue;
private ThreadFactory mThreadFactory;
// The worker
private Thread mThread;
@@ -15,54 +15,24 @@
~ You should have received a copy of the GNU General Public License along with EhViewer.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.7425"
android:scaleY="0.7425"
android:translateX="30.68"
android:translateY="30.68">
<path
android:pathData="M11,9H34"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#FBBD06"
android:strokeLineCap="round"/>
<path
android:pathData="M11,54H23"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#FBBD06"
android:strokeLineCap="round"/>
<path
android:pathData="M11,9L11,54"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#35AA53"
android:strokeLineCap="round"/>
<path
android:pathData="M11,31L52,31"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#FBBD06"
android:strokeLineCap="round"/>
<path
android:pathData="M52,31L52,54"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#4586F3"
android:strokeLineCap="round"/>
<path
android:pathData="M34,21L34,54"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#EB4334"
android:strokeLineCap="round"/>
<path
android:pathData="M15,35H11C8.791,35 7,33.209 7,31C7,28.791 8.791,27 11,27H15V35Z"
android:fillColor="#FBBD06"
android:fillType="evenOdd"/>
</group>
<group
android:scaleX="0.37947997"
android:scaleY="0.37947997"
android:translateX="35.1"
android:translateY="39.584126">
<path
android:fillColor="@color/ic_launcher_foreground"
android:fillType="evenOdd"
android:pathData="M39.893,68.066L39.893,75.977L0,75.977L0,4.59L39.893,4.59L39.893,12.5L8.984,12.5L8.984,34.766L38.086,34.766L38.086,42.578L8.984,42.578L8.984,68.066L39.893,68.066Z" />
<path
android:fillColor="@color/ic_launcher_foreground"
android:fillType="evenOdd"
android:pathData="M99.609,75.977L90.918,75.977L90.918,41.699A23.436,23.436 0,0 0,90.662 38.109Q90.055,34.207 88.013,31.958Q85.629,29.334 81.028,28.863A20.84,20.84 0,0 0,78.906 28.76A23.134,23.134 0,0 0,74.45 29.16Q71.926,29.656 69.994,30.767A11.122,11.122 0,0 0,66.968 33.301A13.32,13.32 0,0 0,64.756 37.276Q63.399,40.971 63.213,46.495A53.517,53.517 0,0 0,63.184 48.291L63.184,75.977L54.395,75.977L54.395,0L63.184,0L63.184,22.266A66.935,66.935 0,0 1,63.115 25.383Q63.043,26.914 62.897,28.251A33.547,33.547 0,0 1,62.695 29.785L63.281,29.785A16.11,16.11 0,0 1,69.394 24.035A19.448,19.448 0,0 1,70.068 23.682A20.88,20.88 0,0 1,77.292 21.622A25.763,25.763 0,0 1,79.98 21.484A32.643,32.643 0,0 1,85.483 21.917Q91.249,22.905 94.678,26.147Q98.472,29.735 99.347,36.631A35.274,35.274 0,0 1,99.609 41.064L99.609,75.977Z" />
</group>
</vector>
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 GeneralPublic 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.7425"
android:scaleY="0.7425"
android:translateX="30.68"
android:translateY="30.68">
<path
android:pathData="M11,9H34"
android:strokeAlpha="0.8"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M11,54H23"
android:strokeAlpha="0.8"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M11,9L11,54"
android:strokeAlpha="0.8"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M11,31L52,31"
android:strokeAlpha="0.8"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M52,31L52,54"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M34,21L34,54"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M15,35H11C8.791,35 7,33.209 7,31C7,28.791 8.791,27 11,27H15V35Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>
@@ -84,10 +84,10 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:divider="@drawable/spacer_keyline"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:divider="@drawable/spacer_keyline"
android:orientation="horizontal"
android:showDividers="middle">
<FrameLayout
@@ -230,8 +230,8 @@
android:clickable="true"
android:focusable="true"
android:insetLeft="40dp"
android:insetTop="16dp"
android:insetRight="40dp"
android:insetTop="16dp"
android:text="@string/read">
<requestFocus />
@@ -244,11 +244,11 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:nextFocusDown="@+id/torrent"
android:focusable="true"
android:insetLeft="40dp"
android:insetTop="16dp"
android:insetRight="40dp"
android:nextFocusDown="@+id/torrent" />
android:insetRight="40dp" />
<android.widget.Space
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff. Show more