Compare commits

..

12 Commits

Author SHA1 Message Date
tom5079
19744dab37 Merge remote-tracking branch 'origin/master' into master 2020-10-04 23:04:30 +09:00
tom5079
12d58e5aa7 Don't cancel download onPause 2020-10-04 23:04:12 +09:00
tom5079
e46dd37a26 Update README.md 2020-10-04 22:47:45 +09:00
tom5079
49c3ebc36b Concurrency issue fixed
Don't cancel download onPause
Limit folder length to 127 characters
2020-10-04 22:44:31 +09:00
tom5079
11e9bc2235 Added five entries per page option 2020-10-04 20:39:33 +09:00
tom5079
3029b3bf0e Update README.md 2020-10-03 20:20:41 +09:00
tom5079
9a6c6f67ce Merge branch 'dev' into master 2020-10-03 20:18:53 +09:00
tom5079
a6ed0baef2 Fix auto cache cleanup 2020-10-03 20:18:20 +09:00
tom5079
d3b43d80da Update README.md 2020-10-03 10:06:17 +09:00
tom5079
46d4316d49 Merge remote-tracking branch 'origin/master' into master 2020-10-03 10:05:30 +09:00
tom5079
ade2864351 Fix auto cache cleanup 2020-10-03 10:03:57 +09:00
tom5079
365fc56e9d Update README.md 2020-10-02 13:09:46 +09:00
15 changed files with 91 additions and 112 deletions

View File

@@ -2,7 +2,7 @@
*Pupil, Hitomi.la viewer for Android*
![](https://img.shields.io/github/downloads/tom5079/Pupil/total)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/latest/Pupil-v5.1.1-hotfix3.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.1.1-hotfix3/Pupil-v5.1.1-hotfix3.apk)
[![](https://img.shields.io/github/downloads/tom5079/Pupil/latest/Pupil-v5.1.3.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.1.3/Pupil-v5.1.3.apk)
[![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v)
# Features

View File

@@ -37,8 +37,8 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 30
versionCode 61
versionName "5.1.2"
versionCode 62
versionName "5.1.3-hotfix1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
@@ -51,7 +51,6 @@ android {
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
buildConfigField("Boolean", "CENSOR", "false")
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
ext.enableCrashlytics = false
@@ -61,7 +60,6 @@ android {
minifyEnabled true
shrinkResources true
buildConfigField("Boolean", "CENSOR", "false")
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
@@ -79,13 +77,12 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0-RC2"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.activity:activity-ktx:1.2.0-alpha08"
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha08"
implementation "androidx.activity:activity-ktx:1.2.0-beta01"
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "androidx.gridlayout:gridlayout:1.0.0"
@@ -94,12 +91,12 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.google.android.material:material:1.3.0-alpha02"
implementation "com.google.android.material:material:1.3.0-alpha03"
implementation "com.google.firebase:firebase-core:17.5.0"
implementation "com.google.firebase:firebase-analytics:17.5.0"
implementation "com.google.firebase:firebase-crashlytics:17.2.1"
implementation "com.google.firebase:firebase-perf:19.0.8"
implementation "com.google.firebase:firebase-crashlytics:17.2.2"
implementation "com.google.firebase:firebase-perf:19.0.9"
implementation "com.google.android.gms:play-services-oss-licenses:17.0.0"
implementation "com.google.android.gms:play-services-mlkit-face-detection:16.1.1"
@@ -126,7 +123,7 @@ dependencies {
implementation "ru.noties.markwon:core:3.1.0"
implementation "xyz.quaver:libpupil:1.7.2"
implementation "xyz.quaver:documentfilex:0.3"
implementation "xyz.quaver:documentfilex:0.3.1"
implementation "xyz.quaver:floatingsearchview:1.0.7"
testImplementation "junit:junit:4.13"

View File

@@ -22,21 +22,6 @@
-dontobfuscate
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
*** rewind();
}
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.SerializationKt
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; } # <-- change package name to your app's
@@ -47,5 +32,4 @@
kotlinx.serialization.KSerializer serializer(...);
}
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
-keep class xyz.quaver.pupil.util.Preferences
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment

View File

@@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 61,
"versionName": "5.1.2",
"versionCode": 62,
"versionName": "5.1.3-hotfix1",
"enabled": true,
"outputFile": "app-release.apk"
}

View File

@@ -126,6 +126,7 @@ class GalleryBlockAdapter(private val galleries: List<Int>) : RecyclerSwipeAdapt
fun bind(galleryID: Int) {
this.galleryID = galleryID
updateProgress(view.context)
val cache = Cache.getInstance(view.context, galleryID)

View File

@@ -27,7 +27,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -37,10 +36,7 @@ import okhttp3.Callback
import okhttp3.Response
import okhttp3.ResponseBody
import okio.*
import xyz.quaver.pupil.PupilInterceptor
import xyz.quaver.pupil.R
import xyz.quaver.pupil.client
import xyz.quaver.pupil.interceptors
import xyz.quaver.pupil.*
import xyz.quaver.pupil.ui.ReaderActivity
import xyz.quaver.pupil.util.cleanCache
import xyz.quaver.pupil.util.downloader.Cache
@@ -49,6 +45,7 @@ import xyz.quaver.pupil.util.ellipsize
import xyz.quaver.pupil.util.normalizeID
import xyz.quaver.pupil.util.requestBuilders
import java.io.IOException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.ceil
import kotlin.math.log10
@@ -91,7 +88,7 @@ class DownloadService : Service() {
PendingIntent.FLAG_UPDATE_CURRENT),
).build()
notification.put(galleryID, NotificationCompat.Builder(this, "download").apply {
notification[galleryID] = NotificationCompat.Builder(this, "download").apply {
setContentTitle(getString(R.string.reader_loading))
setContentText(getString(R.string.reader_notification_text))
setSmallIcon(R.drawable.ic_notification)
@@ -99,7 +96,7 @@ class DownloadService : Service() {
addAction(action)
setProgress(0, 0, true)
setOngoing(true)
})
}
notify(galleryID)
}
@@ -124,7 +121,7 @@ class DownloadService : Service() {
.setProgress(max, progress, false)
.setContentText("$progress/$max")
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null)
if (DownloadManager.getInstance(this).getDownloadFolder(galleryID) != null || galleryID == priority)
notification.let { notificationManager.notify(galleryID, it.build()) }
else
notificationManager.cancel(galleryID)
@@ -199,6 +196,7 @@ class DownloadService : Service() {
* Float.POSITIVE_INFINITY -> Download completed
*/
val progress = ConcurrentHashMap<Int, MutableList<Float>>()
var priority = 0
fun isCompleted(galleryID: Int) = progress[galleryID]?.toList()?.all { it == Float.POSITIVE_INFINITY } == true
@@ -290,16 +288,16 @@ class DownloadService : Service() {
fun delete(galleryID: Int, startId: Int? = null) = CoroutineScope(Dispatchers.IO).launch {
cancel(galleryID)
DownloadManager.getInstance(this@DownloadService).deleteDownloadFolder(galleryID)
Cache.delete(galleryID)
Cache.delete(this@DownloadService, galleryID)
startId?.let { stopSelf(it) }
}
fun download(galleryID: Int, priority: Boolean = false, startId: Int? = null): Job = CoroutineScope(Dispatchers.IO).launch {
cleanCache(this@DownloadService)
if (DownloadManager.getInstance(this@DownloadService).isDownloading(galleryID))
return@launch
if (progress.containsKey(galleryID))
cancel(galleryID)
cleanCache(this@DownloadService)
val cache = Cache.getInstance(this@DownloadService, galleryID)
@@ -314,6 +312,8 @@ class DownloadService : Service() {
return@launch
}
histories.add(galleryID)
progress[galleryID] = MutableList(reader.galleryInfo.files.size) { 0F }
cache.metadata.imageList?.let {

View File

@@ -378,7 +378,7 @@ class MainActivity :
if (v !is CardView)
return@listener false
val galleryID = galleries[position]
val galleryID = galleries.getOrNull(position) ?: return@listener true
GalleryDialog(this@MainActivity, galleryID).apply {
onChipClickedHandler.add {

View File

@@ -33,11 +33,9 @@ import android.view.animation.Animation
import android.view.animation.AnticipateInterpolator
import android.view.animation.OvershootInterpolator
import android.view.animation.TranslateAnimation
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
@@ -59,7 +57,6 @@ import xyz.quaver.Code
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.ReaderAdapter
import xyz.quaver.pupil.favorites
import xyz.quaver.pupil.histories
import xyz.quaver.pupil.services.DownloadService
import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.camera
@@ -67,8 +64,6 @@ import xyz.quaver.pupil.util.closeCamera
import xyz.quaver.pupil.util.downloader.Cache
import xyz.quaver.pupil.util.downloader.DownloadManager
import xyz.quaver.pupil.util.startCamera
import java.util.*
import kotlin.concurrent.schedule
class ReaderActivity : BaseActivity() {
@@ -88,6 +83,8 @@ class ReaderActivity : BaseActivity() {
private val conn = object: ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
downloader = (service as DownloadService.Binder).service.also {
it.priority = 0
if (!it.progress.containsKey(galleryID))
DownloadService.download(this@ReaderActivity, galleryID, true)
}
@@ -230,14 +227,16 @@ class ReaderActivity : BaseActivity() {
if (downloader != null)
unbindService(conn)
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
DownloadService.cancel(this, galleryID)
downloader?.priority = galleryID
}
override fun onDestroy() {
super.onDestroy()
update = false
if (!DownloadManager.getInstance(this).isDownloading(galleryID))
DownloadService.cancel(this, galleryID)
}
override fun onBackPressed() {
@@ -292,8 +291,6 @@ class ReaderActivity : BaseActivity() {
return@launch
}
histories.add(galleryID)
reader_download_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0
reader_download_progressbar.progress =
downloader.progress[galleryID]?.count { it.isInfinite() } ?: 0

View File

@@ -77,7 +77,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleryID)
})
histories.add(galleryID)
}
}
@@ -238,7 +237,6 @@ class GalleryDialog(context: Context, private val galleryID: Int) : AlertDialog(
context.startActivity(Intent(context, ReaderActivity::class.java).apply {
putExtra("galleryID", galleries[position])
})
histories.add(galleries[position])
}
onItemLongClickListener = { _, position, _ ->
GalleryDialog(context, galleries[position]).apply {

View File

@@ -22,8 +22,9 @@ import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import java.io.File
import java.util.*
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = mutableSetOf()) : MutableSet<T> by set {
class SavedSet <T: Any> (private val file: File, private val any: T, private val set: MutableSet<T> = Collections.synchronizedSet(mutableSetOf())) : MutableSet<T> by set {
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)

View File

@@ -24,6 +24,8 @@ import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
@@ -37,6 +39,7 @@ import xyz.quaver.io.FileX
import xyz.quaver.io.util.*
import xyz.quaver.pupil.client
import xyz.quaver.pupil.util.Preferences
import java.io.File
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
@@ -60,8 +63,8 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
}
@Synchronized
fun delete(galleryID: Int) {
instances[galleryID]?.cacheFolder?.deleteRecursively()
fun delete(context: Context, galleryID: Int) {
File(context.cacheDir, "imageCache/$galleryID").deleteRecursively()
instances.remove(galleryID)
}
}
@@ -142,7 +145,12 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
client.newCall(request).execute().also { if (it.code() != 200) throw IOException() }.body()?.use { it.bytes() }
}.getOrNull()?.let { thumbnail -> kotlin.runCatching {
cacheFolder.getChild(".thumbnail").also { it.writeBytes(thumbnail) }
cacheFolder.getChild(".thumbnail").also {
if (!it.exists())
it.createNewFile()
it.writeBytes(thumbnail)
}
}.getOrNull()?.uri }
} } ?: Uri.EMPTY
@@ -198,56 +206,64 @@ class Cache private constructor(context: Context, val galleryID: Int) : ContextW
setMetadata { metadata -> metadata.imageList!![index] = fileName }
}
private val lock = ConcurrentHashMap<Int, Mutex>()
@Suppress("BlockingMethodInNonBlockingContext")
fun moveToDownload() = CoroutineScope(Dispatchers.IO).launch {
val downloadFolder = downloadFolder ?: return@launch
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder.getChild(".metadata")
if (downloadMetadata.exists() || !cacheMetadata.exists())
if (lock[galleryID]?.isLocked == true)
return@launch
if (cacheMetadata.exists()) {
kotlin.runCatching {
downloadMetadata.createNewFile()
downloadMetadata.writeText(Json.encodeToString(metadata))
(lock[galleryID] ?: Mutex().also { lock[galleryID] = it }).withLock {
val cacheMetadata = cacheFolder.getChild(".metadata")
val downloadMetadata = downloadFolder.getChild(".metadata")
cacheMetadata.delete()
if (!cacheMetadata.exists())
return@launch
if (cacheMetadata.exists()) {
kotlin.runCatching {
if (!downloadMetadata.exists())
downloadMetadata.createNewFile()
downloadMetadata.writeText(Json.encodeToString(metadata))
}
}
}
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
val cacheThumbnail = cacheFolder.getChild(".thumbnail")
val downloadThumbnail = downloadFolder.getChild(".thumbnail")
if (cacheThumbnail.exists() && !downloadThumbnail.exists()) {
kotlin.runCatching {
if (!downloadThumbnail.exists())
downloadThumbnail.createNewFile()
if (cacheThumbnail.exists()) {
kotlin.runCatching {
if (!downloadThumbnail.exists())
downloadThumbnail.createNewFile()
downloadThumbnail.outputStream()?.use { target -> cacheThumbnail.inputStream()?.use { source ->
source.copyTo(target)
} }
cacheThumbnail.delete()
downloadThumbnail.outputStream()?.use { target -> target.channel.truncate(0L); cacheThumbnail.inputStream()?.use { source ->
source.copyTo(target)
} }
cacheThumbnail.delete()
}
}
}
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName)
metadata.imageList?.forEach { imageName ->
imageName ?: return@forEach
val target = downloadFolder.getChild(imageName)
val source = cacheFolder.getChild(imageName)
if (!source.exists() || target.exists())
return@forEach
if (!source.exists())
return@forEach
kotlin.runCatching {
if (!target.exists())
target.createNewFile()
kotlin.runCatching {
if (!target.exists())
target.createNewFile()
target.outputStream()?.use { target -> source.inputStream()?.use { source ->
source.copyTo(target)
} }
target.outputStream()?.use { target -> target.channel.truncate(0L); source.inputStream()?.use { source ->
source.copyTo(target)
} }
}
}
cacheFolder.deleteRecursively()
}
}
}

View File

@@ -37,22 +37,6 @@ fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
val cacheFolder = File(context.cacheDir, "imageCache")
val downloadManager = DownloadManager.getInstance(context)
cacheFolder.listFiles { file ->
val galleryID = file.name.toIntOrNull() ?: return@listFiles true
!(downloadManager.downloadFolderMap.containsKey(galleryID) || histories.contains(galleryID))
}?.forEach {
it.deleteRecursively()
}
DownloadManager.getInstance(context).downloadFolderMap.keys.forEach {
val folder = File(cacheFolder, it.toString())
if (!downloadManager.isDownloading(it) && folder.exists()) {
folder.deleteRecursively()
}
}
val limit = (Preferences.get<String>("cache_limit").toLongOrNull() ?: 0L)*1024*1024*1024
if (limit == 0L) return@withLock
@@ -74,7 +58,7 @@ fun cleanCache(context: Context) = CoroutineScope(Dispatchers.IO).launch {
(histories.firstOrNull {
caches.contains(it.toString()) && !downloadManager.isDownloading(it)
} ?: return@withLock).let {
Cache.delete(it)
Cache.delete(context, it)
}
}
}

View File

@@ -93,14 +93,14 @@ fun GalleryBlock.formatDownloadFolder(): String =
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace(Regex("""[*\\|"?><:/]"""), "")
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
fun GalleryBlock.formatDownloadFolderTest(format: String): String =
format.let {
formatMap.entries.fold(it) { str, (k, v) ->
str.replace(k, v.invoke(this), true)
}
}.replace(Regex("""[*\\|"?><:/]"""), "")
}.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
val Reader.requestBuilders: List<Request.Builder>
get() {

View File

@@ -313,7 +313,7 @@ fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() {
)
synchronized(Cache) {
Cache.delete(galleryID)
Cache.delete(this@migrate, galleryID)
}
downloadFolderMap[galleryID] = folder.name

View File

@@ -2,6 +2,7 @@
<resources>
<string-array name="settings_galleries_per_page">
<item>5</item>
<item>10</item>
<item>25</item>
<item>50</item>