Compare commits

...

5 Commits

Author SHA1 Message Date
Pupil
e810abe33a Bug fix 2020-02-09 17:57:18 +09:00
Pupil
6172a73719 Bug fix 2020-02-09 17:36:28 +09:00
Pupil
7455e68a45 Bug fix 2020-02-09 17:35:34 +09:00
Pupil
748495ca64 Downloader thread number to 4 2020-02-09 17:25:55 +09:00
Pupil
f6d9c7f550 Bug fix
Networking optimized
2020-02-09 17:11:35 +09:00
15 changed files with 111 additions and 80 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 33 versionCode 36
versionName "5.0" versionName "5.3-beta3"
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":33,"versionName":"5.0","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":36,"versionName":"5.3-beta3","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

View File

@@ -22,7 +22,7 @@
tools:replace="android:theme"> tools:replace="android:theme">
<provider <provider
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.provider"
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">

View File

@@ -49,7 +49,11 @@ class Pupil : MultiDexApplication() {
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json")) histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json")) favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
val download = preference.getString("dl_location", null) val download = try {
preference.getString("dl_location", null)
} catch (e: Exception) {
preference.edit().remove("dl_location").apply()
}
if (download == null) { if (download == null) {
val default = ContextCompat.getExternalFilesDirs(this, null)[0] val default = ContextCompat.getExternalFilesDirs(this, null)[0]

View File

@@ -129,7 +129,12 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
}) })
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val thumbnail = Base64.decode(Cache(context).getThumbnail(galleryBlock.id), Base64.DEFAULT) val thumbnail = Cache(context).getThumbnail(galleryBlock.id).let {
if (it != null)
Base64.decode(it, Base64.DEFAULT)
else
null
}
glide glide
.load(thumbnail) .load(thumbnail)

View File

@@ -26,7 +26,8 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.android.material.snackbar.Snackbar import com.crashlytics.android.Crashlytics
import io.fabric.sdk.android.Fabric
import kotlinx.android.synthetic.main.item_reader.view.* import kotlinx.android.synthetic.main.item_reader.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -108,17 +109,13 @@ 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?.isNaN() == true) { if (progress?.isNaN() == true) {
if (Fabric.isInitialized())
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
glide glide
.load(R.drawable.image_broken_variant) .load(R.drawable.image_broken_variant)
.into(holder.view.image) .into(holder.view.image)
Snackbar
.make(
holder.view,
DownloadWorker.getInstance(context).exception[galleryID]!![position]?.message
?: context.getText(R.string.default_error_msg),
Snackbar.LENGTH_INDEFINITE
)
.show()
return return
} }

View File

@@ -162,7 +162,7 @@ class ReaderActivity : AppCompatActivity() {
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false) val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
with(view.dialog_number_picker) { with(view.dialog_number_picker) {
minValue=1 minValue=1
maxValue=reader_recyclerview.adapter?.itemCount ?: 0 maxValue=reader_recyclerview?.adapter?.itemCount ?: 0
value=currentPage value=currentPage
} }
val dialog = AlertDialog.Builder(this).apply { val dialog = AlertDialog.Builder(this).apply {
@@ -196,7 +196,7 @@ class ReaderActivity : AppCompatActivity() {
super.onDestroy() super.onDestroy()
timer.cancel() timer.cancel()
(reader_recyclerview.adapter as ReaderAdapter).timer.cancel() (reader_recyclerview?.adapter as? ReaderAdapter)?.timer?.cancel()
if (!Cache(this).isDownloading(galleryID)) if (!Cache(this).isDownloading(galleryID))
DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID) DownloadWorker.getInstance(this@ReaderActivity).cancel(galleryID)

View File

@@ -101,7 +101,7 @@ class SettingsFragment :
}.show() }.show()
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context)!! val dir = getDownloadDirectory(context)
AlertDialog.Builder(context).apply { AlertDialog.Builder(context).apply {
setTitle(R.string.warning) setTitle(R.string.warning)
@@ -150,7 +150,7 @@ class SettingsFragment :
"backup" -> { "backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
context, context,
getDownloadDirectory(context)?.createFile("null", "favorites.json")!! getDownloadDirectory(context).createFile("null", "favorites.json")!!
) )
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG) Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -192,8 +192,7 @@ class SettingsFragment :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
"dl_location" -> { "dl_location" -> {
findPreference<Preference>(key)?.summary = findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path
FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
} }
} }
} }
@@ -230,7 +229,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"delete_downloads" -> { "delete_downloads" -> {
val dir = getDownloadDirectory(context)!! val dir = getDownloadDirectory(context)
summary = getDirSize(dir) summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
@@ -242,7 +241,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }
"dl_location" -> { "dl_location" -> {
summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri) summary = getDownloadDirectory(context).uri.path
onPreferenceClickListener = this@SettingsFragment onPreferenceClickListener = this@SettingsFragment
} }

View File

@@ -23,11 +23,15 @@ import android.content.ContextWrapper
import android.util.Base64 import android.util.Base64
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import kotlinx.serialization.ImplicitReflectionSerializer import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.parse import kotlinx.serialization.parse
import kotlinx.serialization.stringify import kotlinx.serialization.stringify
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.pupil.util.* import xyz.quaver.pupil.util.*
@@ -41,7 +45,7 @@ class Cache(context: Context) : ContextWrapper(context) {
// Search in this order // Search in this order
// Download -> Cache // Download -> Cache
fun getCachedGallery(galleryID: Int) : DocumentFile? { fun getCachedGallery(galleryID: Int) : DocumentFile? {
var file = getDownloadDirectory(this)?.findFile(galleryID.toString()) var file = getDownloadDirectory(this).findFile(galleryID.toString())
if (file?.exists() == true) if (file?.exists() == true)
return file return file
@@ -107,17 +111,21 @@ class Cache(context: Context) : ContextWrapper(context) {
suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? { suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? {
val metadata = Cache(this).getCachedMetadata(galleryID) val metadata = Cache(this).getCachedMetadata(galleryID)
val source = mapOf(
Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) },
Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
)
val galleryBlock = if (metadata?.galleryBlock == null) val galleryBlock = if (metadata?.galleryBlock == null)
listOf( source.entries.map {
{ xyz.quaver.hitomi.getGalleryBlock(galleryID) },
{ xyz.quaver.hiyobi.getGalleryBlock(galleryID) }
).map {
CoroutineScope(Dispatchers.IO).async { CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching { kotlin.runCatching {
it.invoke() it.value.invoke()
}.getOrNull() }.getOrNull()
} }
}.awaitAll().filterNotNull() }.firstOrNull {
it.await() != null
}?.await()
else else
metadata.galleryBlock metadata.galleryBlock
@@ -126,52 +134,56 @@ class Cache(context: Context) : ContextWrapper(context) {
Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock) Metadata(Cache(this).getCachedMetadata(galleryID), galleryBlock = galleryBlock)
) )
val mirrors = preference.getString("mirrors", "")!!.split('>') return galleryBlock
return galleryBlock.firstOrNull {
mirrors.contains(it.code.name)
} ?: galleryBlock.firstOrNull()
} }
fun getReaderOrNull(galleryID: Int): Reader? { fun getReaderOrNull(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID) return getCachedMetadata(galleryID)?.reader
val mirrors = preference.getString("mirrors", "")!!.split('>')
return metadata?.readers?.firstOrNull {
mirrors.contains(it.code.name)
} ?: metadata?.readers?.firstOrNull()
} }
suspend fun getReader(galleryID: Int): Reader? { suspend fun getReader(galleryID: Int): Reader? {
val metadata = getCachedMetadata(galleryID) val metadata = getCachedMetadata(galleryID)
val mirrors = preference.getString("mirrors", null)?.split('>') ?: listOf()
val readers = if (metadata?.readers == null) { val sources = mapOf(
listOf( Code.HITOMI to { xyz.quaver.hitomi.getReader(galleryID) },
{ xyz.quaver.hitomi.getReader(galleryID) }, Code.HIYOBI to { xyz.quaver.hiyobi.getReader(galleryID) }
{ xyz.quaver.hiyobi.getReader(galleryID) } ).let {
).map { if (mirrors.isNotEmpty())
it.toSortedMap(
Comparator { o1, o2 ->
mirrors.indexOf(o1.name) - mirrors.indexOf(o2.name)
}
)
else
it
}
val reader = if (metadata?.reader == null) {
CoroutineScope(Dispatchers.IO).async { CoroutineScope(Dispatchers.IO).async {
kotlin.runCatching { var retval: Reader? = null
it.invoke()
for (source in sources) {
retval = kotlin.runCatching {
source.value.invoke()
}.getOrNull() }.getOrNull()
}
}.awaitAll().filterNotNull() if (retval != null)
} else { break
metadata.readers
} }
if (readers.isNotEmpty()) retval
}.await()
} else
metadata.reader
if (reader != null)
setCachedMetadata( setCachedMetadata(
galleryID, galleryID,
Metadata(Cache(this).getCachedMetadata(galleryID), readers = readers) Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader)
) )
val mirrors = preference.getString("mirrors", "")!!.split('>') return reader
return readers.firstOrNull {
mirrors.contains(it.code.name)
} ?: readers.firstOrNull()
} }
fun getImages(galleryID: Int): List<DocumentFile?>? { fun getImages(galleryID: Int): List<DocumentFile?>? {
@@ -180,7 +192,7 @@ class Cache(context: Context) : ContextWrapper(context) {
val images = gallery.listFiles() val images = gallery.listFiles()
return reader.galleryInfo.indices.map { index -> return reader.galleryInfo.indices.map { index ->
images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true } images.firstOrNull { file -> file.name?.startsWith("%05d".format(index)) == true }
} }
} }
@@ -201,14 +213,14 @@ class Cache(context: Context) : ContextWrapper(context) {
val cache = getCachedGallery(galleryID) val cache = getCachedGallery(galleryID)
if (cache != null) { if (cache != null) {
val download = getDownloadDirectory(this)!! val download = getDownloadDirectory(this)
if (!download.isParentOf(cache)) { if (!download.isParentOf(cache)) {
cache.copyRecursively(this, download) cache.copyRecursively(this, download)
cache.deleteRecursively() cache.deleteRecursively()
} }
} else } else
getDownloadDirectory(this)?.createDirectory(galleryID.toString()) getDownloadDirectory(this).createDirectory(galleryID.toString())
} }
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true

View File

@@ -162,7 +162,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
.body(ProgressResponseBody(request.tag(), response.body(), progressListener)) .body(ProgressResponseBody(request.tag(), response.body(), progressListener))
.build() .build()
} }
.dispatcher(Dispatcher(Executors.newSingleThreadExecutor())) .dispatcher(Dispatcher(Executors.newFixedThreadPool(4)))
.build() .build()
fun stop() { fun stop() {
@@ -279,6 +279,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList()) progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList()) exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
if (notification[galleryID] == null)
initNotification(galleryID)
notification[galleryID].setContentTitle(reader.title) notification[galleryID].setContentTitle(reader.title)
notify(galleryID) notify(galleryID)
@@ -309,7 +312,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
val ext = val ext =
call.request().url().encodedPath().split('.').last() call.request().url().encodedPath().split('.').last()
Cache(this@DownloadWorker).putImage(galleryID, "$i.$ext", res) Cache(this@DownloadWorker).putImage(galleryID, "%05d.%s".format(i, ext), res)
progress[galleryID]?.set(i, Float.POSITIVE_INFINITY) progress[galleryID]?.set(i, Float.POSITIVE_INFINITY)
} }

View File

@@ -25,20 +25,20 @@ import xyz.quaver.hitomi.Reader
@Serializable @Serializable
data class Metadata( data class Metadata(
val thumbnail: String? = null, val thumbnail: String? = null,
val galleryBlock: List<GalleryBlock>? = null, val galleryBlock: GalleryBlock? = null,
val readers: List<Reader>? = null, val reader: Reader? = null,
val isDownloading: Boolean? = null val isDownloading: Boolean? = null
) { ) {
constructor( constructor(
metadata: Metadata?, metadata: Metadata?,
thumbnail: String? = null, thumbnail: String? = null,
galleryBlock: List<GalleryBlock>? = null, galleryBlock: GalleryBlock? = null,
readers: List<Reader>? = null, readers: Reader? = null,
isDownloading: Boolean? = null isDownloading: Boolean? = null
) : this( ) : this(
thumbnail ?: metadata?.thumbnail, thumbnail ?: metadata?.thumbnail,
galleryBlock ?: metadata?.galleryBlock, galleryBlock ?: metadata?.galleryBlock,
readers ?: metadata?.readers, readers ?: metadata?.reader,
isDownloading ?: metadata?.isDownloading isDownloading ?: metadata?.isDownloading
) )
} }

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.io.File import java.io.File
@@ -28,20 +29,29 @@ import java.nio.charset.Charset
import java.util.* import java.util.*
fun getCachedGallery(context: Context, galleryID: Int) = fun getCachedGallery(context: Context, galleryID: Int) =
getDownloadDirectory(context)?.findFile(galleryID.toString()) ?: getDownloadDirectory(context).findFile(galleryID.toString()) ?:
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID")) DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
fun getDownloadDirectory(context: Context) : DocumentFile? { fun getDownloadDirectory(context: Context) : DocumentFile {
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let { val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
if (it != null)
Uri.parse(it) Uri.parse(it)
else
Uri.fromFile(context.getExternalFilesDir(null))
} }
return if (uri.toString().startsWith("file")) return if (uri.toString().startsWith("file"))
DocumentFile.fromFile(File(uri.path!!)) DocumentFile.fromFile(File(uri.path!!))
else else
DocumentFile.fromTreeUri(context, uri) DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
} }
fun convertUpdateUri(context: Context, uri: Uri) : Uri =
if (uri.toString().startsWith("file"))
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
else
uri
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) { fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
context.contentResolver.openOutputStream(to.uri).use { out -> context.contentResolver.openOutputStream(to.uri).use { out ->
out!! out!!

View File

@@ -168,7 +168,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val install = Intent(Intent.ACTION_VIEW).apply { val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")) setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
} }
builder.apply { builder.apply {

View File

@@ -17,6 +17,7 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>. ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <paths>
<external-path name="external" path="/"/> <external-path name="external" path="/"/>
<external-files-path name="files" path="/"/>
</paths> </paths>

View File

@@ -75,7 +75,7 @@ class UnitTest {
@Test @Test
fun test_getReader() { fun test_getReader() {
val reader = getReader(1442740) val reader = getReader(1567569)
print(reader) print(reader)
} }