Compare commits

...

7 Commits

Author SHA1 Message Date
Pupil
dfe435c4f3 Merge pull request #70 from tom5079/dev
Version 4.9
2020-02-24 21:11:03 +09:00
Pupil
69e85f8b90 Bug fix 2020-02-24 21:10:10 +09:00
Pupil
c9bde3c487 Merge pull request #69 from tom5079/dev
Version 4.8
2020-02-24 20:48:55 +09:00
Pupil
65e9557d9f Bug fix 2020-02-24 20:02:44 +09:00
Pupil
4f249c07e7 Merge pull request #68 from tom5079/dev
Version 4.7
2020-02-24 12:49:56 +09:00
Pupil
5fd35b492c Bug fix 2020-02-24 12:49:19 +09:00
Pupil
9bddf95013 Image loading fixed 2020-02-23 21:18:19 +09:00
8 changed files with 159 additions and 33 deletions

View File

@@ -19,8 +19,8 @@ android {
applicationId "xyz.quaver.pupil" applicationId "xyz.quaver.pupil"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 43 versionCode 45
versionName "4.7-beta1" versionName "4.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":43,"versionName":"4.7-beta1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":45,"versionName":"4.9","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -28,7 +28,6 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.* import kotlinx.android.synthetic.main.item_reader.view.*
@@ -64,8 +63,7 @@ class ReaderAdapter(private val context: Context,
override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? { override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
return glide return glide
.load(item) .load(item)
.diskCacheStrategy(DiskCacheStrategy.NONE) .fitCenter()
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.apply { .apply {
if (BuildConfig.CENSOR) if (BuildConfig.CENSOR)
@@ -114,7 +112,7 @@ class ReaderAdapter(private val context: Context,
if (!isFullScreen) if (!isFullScreen)
(holder.view.container.layoutParams as ConstraintLayout.LayoutParams) (holder.view.container.layoutParams as ConstraintLayout.LayoutParams)
.dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}" .dimensionRatio = "W,${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}"
holder.view.reader_index.text = (position+1).toString() holder.view.reader_index.text = (position+1).toString()
@@ -122,17 +120,21 @@ class ReaderAdapter(private val context: Context,
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
if (progress?.isInfinite() == true && images != null) { if (progress?.isInfinite() == true && images != null) {
holder.view.reader_item_progressbar.visibility = View.INVISIBLE
holder.view.image.post {
glide glide
.load(images) .load(images)
.diskCacheStrategy(DiskCacheStrategy.NONE) .fitCenter()
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant) .error(R.drawable.image_broken_variant)
.apply {
if (BuildConfig.CENSOR)
override(5, 8)
}
.into(holder.view.image) .into(holder.view.image)
}
} else { } else {
holder.view.reader_item_progressbar.visibility = View.VISIBLE
glide.clear(holder.view.image)
if (progress?.isNaN() == true) { if (progress?.isNaN() == true) {
if (Fabric.isInitialized()) if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))

View File

@@ -961,7 +961,9 @@ class MainActivity : AppCompatActivity() {
} }
Mode.DOWNLOAD -> { Mode.DOWNLOAD -> {
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file -> val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists() file.isDirectory && file.name.toIntOrNull() != null
}?.sortedByDescending {
it.lastModified()
}?.map { }?.map {
it.name.toInt() it.name.toInt()
} ?: emptyList() } ?: emptyList()

View File

@@ -21,6 +21,7 @@ package xyz.quaver.pupil.util.download
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import android.util.SparseArray
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -32,15 +33,32 @@ import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.proxy import xyz.quaver.proxy
import xyz.quaver.pupil.util.copyRecursively
import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getCachedGallery
import xyz.quaver.pupil.util.getDownloadDirectory import xyz.quaver.pupil.util.getDownloadDirectory
import xyz.quaver.pupil.util.json import xyz.quaver.pupil.util.json
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.net.URL import java.net.URL
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
class Cache(context: Context) : ContextWrapper(context) { class Cache(context: Context) : ContextWrapper(context) {
private val locks = SparseArray<Lock>()
private fun lock(galleryID: Int) {
synchronized(locks) {
if (locks.indexOfKey(galleryID) < 0)
locks.put(galleryID, ReentrantLock())
}
locks[galleryID].lock()
}
private fun unlock(galleryID: Int) {
locks[galleryID]?.unlock()
}
private val preference = PreferenceManager.getDefaultSharedPreferences(this) private val preference = PreferenceManager.getDefaultSharedPreferences(this)
// Search in this order // Search in this order
@@ -217,13 +235,16 @@ class Cache(context: Context) : ContextWrapper(context) {
return null return null
} }
fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) { fun putImage(galleryID: Int, index: Int, ext: String, data: InputStream) {
val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also { val cache = File(getCachedGallery(galleryID), "%05d.$ext".format(index)).also {
if (!it.exists()) if (!it.exists())
it.createNewFile() it.createNewFile()
} }
data.copyTo(FileOutputStream(cache)) data.use {
it.copyTo(FileOutputStream(cache))
}
} }
fun moveToDownload(galleryID: Int) { fun moveToDownload(galleryID: Int) {
@@ -233,7 +254,7 @@ class Cache(context: Context) : ContextWrapper(context) {
} }
val download = File(getDownloadDirectory(this), galleryID.toString()) val download = File(getDownloadDirectory(this), galleryID.toString())
cache.copyRecursively(download, true) cache.copyRecursively(download, true) { _, _ -> OnErrorAction.SKIP }
cache.deleteRecursively() cache.deleteRecursively()
} }

View File

@@ -37,13 +37,14 @@ import okio.*
import xyz.quaver.Code import xyz.quaver.Code
import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.Reader
import xyz.quaver.hitomi.getReferer import xyz.quaver.hitomi.getReferer
import xyz.quaver.hitomi.urlFromUrlFromHash import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.cookie
import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.createImgList
import xyz.quaver.hiyobi.user_agent import xyz.quaver.hiyobi.user_agent
import xyz.quaver.proxy import xyz.quaver.proxy
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.ReaderActivity import xyz.quaver.pupil.ui.ReaderActivity
import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
@@ -215,10 +216,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
when (reader.code) { when (reader.code) {
Code.HITOMI -> { Code.HITOMI -> {
url( url(
urlFromUrlFromHash( imageUrlFromImage(
galleryID, galleryID,
reader.galleryInfo.files[index], reader.galleryInfo.files[index],
if (lowQuality) "webp" else null lowQuality
) )
) )
addHeader("Referer", getReferer(galleryID)) addHeader("Referer", getReferer(galleryID))
@@ -256,7 +257,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val cache = Cache(this@DownloadWorker).getImages(galleryID) val cache = Cache(this@DownloadWorker).getImages(galleryID)
progress.put(galleryID, reader.galleryInfo.files.indices.map { index -> progress.put(galleryID, reader.galleryInfo.files.indices.map { index ->
if (cache?.getOrNull(index) != null) if (cache?.firstOrNull { it?.nameWithoutExtension?.toIntOrNull() == index } != null)
Float.POSITIVE_INFINITY Float.POSITIVE_INFINITY
else else
0F 0F
@@ -308,9 +309,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
Log.i("PUPILD", "OK ${call.request().tag()}") Log.i("PUPILD", "OK ${call.request().tag()}")
try {
val ext = call.request().url().encodedPath().split('.').last() val ext = call.request().url().encodedPath().split('.').last()
try {
response.body().use { response.body().use {
Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream()) Cache(this@DownloadWorker).putImage(galleryID, i, ext, it.byteStream())
} }
@@ -332,6 +333,26 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
Log.i("PUPILD", "SUCCESS ${call.request().tag()}") Log.i("PUPILD", "SUCCESS ${call.request().tag()}")
} catch (e: Exception) { } catch (e: Exception) {
progress[galleryID]?.set(i, Float.NaN)
exception[galleryID]?.set(i, e)
notify(galleryID)
CoroutineScope(Dispatchers.IO).launch {
if (isCompleted(galleryID) && clients.indexOfKey(galleryID) >= 0) {
clients.remove(galleryID)
with(Cache(this@DownloadWorker)) {
if (isDownloading(galleryID)) {
moveToDownload(galleryID)
setDownloading(galleryID, false)
}
}
}
}
File(Cache(this@DownloadWorker).getCachedGallery(galleryID), "%05d.$ext".format(i)).delete()
Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})") Log.i("PUPILD", "FAIL ON OK ${call.request().tag()} (${e.message})")
} }
} }
@@ -350,13 +371,17 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val max = progress[galleryID]?.size ?: 0 val max = progress[galleryID]?.size ?: 0
val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0 val progress = progress[galleryID]?.count { !it.isFinite() } ?: 0
if (isCompleted(galleryID)) Log.i("PUPILD", "NOTIFY $galleryID ${isCompleted(galleryID)} $progress/$max")
if (isCompleted(galleryID)) {
notification[galleryID] notification[galleryID]
?.setContentText(getString(R.string.reader_notification_complete)) ?.setContentText(getString(R.string.reader_notification_complete))
?.setSmallIcon(android.R.drawable.stat_sys_download_done) ?.setSmallIcon(android.R.drawable.stat_sys_download_done)
?.setProgress(0, 0, false) ?.setProgress(0, 0, false)
?.setOngoing(false) ?.setOngoing(false)
else
notificationManager.cancel(galleryID)
} else
notification[galleryID] notification[galleryID]
?.setProgress(max, progress, false) ?.setProgress(max, progress, false)
?.setContentText("$progress/$max") ?.setContentText("$progress/$max")
@@ -388,18 +413,27 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
private fun loop() = CoroutineScope(Dispatchers.Default).launch { private fun loop() = CoroutineScope(Dispatchers.Default).launch {
while (true) { while (true) {
if (queue.isEmpty() || clients.size() >= preferences.getInt("max_download", 4)) if (queue.isEmpty())
continue continue
val galleryID = queue.poll() ?: continue val galleryID = queue.peek() ?: continue
if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading! if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading!
continue continue
if (notification[galleryID] == null)
initNotification(galleryID) initNotification(galleryID)
if (Cache(this@DownloadWorker).isDownloading(galleryID)) if (Cache(this@DownloadWorker).isDownloading(galleryID))
notificationManager.notify(galleryID, notification[galleryID].build()) notificationManager.notify(galleryID, notification[galleryID].build())
if (clients.size() >= preferences.getInt("max_download", 4))
continue
Log.i("PUPILD", "QUEUED $galleryID #${clients.size()+1}")
worker.put(galleryID, download(galleryID)) worker.put(galleryID, download(galleryID))
queue.poll()
} }
} }

View File

@@ -26,6 +26,7 @@ import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.io.IOException
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.lang.reflect.Array import java.lang.reflect.Array
@@ -212,3 +213,65 @@ fun Uri.toFile(context: Context): File? {
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName) return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
} }
fun File.copyRecursively(
target: File,
overwrite: Boolean = false,
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
): Boolean {
if (!exists()) {
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
OnErrorAction.TERMINATE
}
try {
// We cannot break for loop from inside a lambda, so we have to use an exception here
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw IOException("Walk failed") }) {
if (!src.exists()) {
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
OnErrorAction.TERMINATE)
return false
} else {
val relPath = src.toRelativeString(this)
val dstFile = File(target, relPath)
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
val stillExists = if (!overwrite) true else {
if (dstFile.isDirectory)
!dstFile.deleteRecursively()
else
!dstFile.delete()
}
if (stillExists) {
if (onError(dstFile, FileAlreadyExistsException(file = src,
other = dstFile,
reason = "The destination file already exists.")) == OnErrorAction.TERMINATE)
return false
continue
}
}
if (src.isDirectory) {
dstFile.mkdirs()
} else {
val length = try {
src.copyTo(dstFile, overwrite).length()
} catch (e: IOException) {
if (onError(src, e) == OnErrorAction.TERMINATE)
return false
else
-1
}
if (length != src.length()) {
if (onError(src, IOException("Source file wasn't copied completely, length of destination file differs.")) == OnErrorAction.TERMINATE)
return false
}
}
}
}
return true
} catch (e: IOException) {
return false
}
}

View File

@@ -25,10 +25,13 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintHeight_max="2000dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="@drawable/reader_item_boundary"> android:background="@drawable/reader_item_boundary">
<LinearLayout <LinearLayout
@@ -61,11 +64,12 @@
<com.github.chrisbanes.photoview.PhotoView <com.github.chrisbanes.photoview.PhotoView
android:id="@+id/image" android:id="@+id/image"
android:contentDescription="@string/reader_imageview_description"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:paddingBottom="8dp"/> android:paddingBottom="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>