diff --git a/app/build.gradle b/app/build.gradle
index 41aff091..1123981c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,15 +19,16 @@ android {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
targetSdkVersion 29
- versionCode 32
- versionName "4.3-beta1-hotfix1"
+ versionCode 33
+ versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
+ shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
buildTypes.each {
@@ -73,9 +74,11 @@ dependencies {
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
implementation 'com.github.clans:fab:1.6.4'
implementation 'com.github.bumptech.glide:glide:4.10.0'
- implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
+ implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
transitive = false
}
+ implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
+ implementation 'com.gu:option:1.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}"
diff --git a/app/release/output.json b/app/release/output.json
index 59883138..28c17fcc 100644
--- a/app/release/output.json
+++ b/app/release/output.json
@@ -1 +1 @@
-[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":32,"versionName":"4.3-hotfix1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
\ No newline at end of file
+[{"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":{}}]
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 26121a52..62e797a8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,7 @@
+ android:maxSdkVersion="21" />
+
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
index 9b90dbbe..90785a19 100644
--- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt
+++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt
@@ -22,6 +22,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
+import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
@@ -30,17 +31,12 @@ import androidx.preference.PreferenceManager
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import xyz.quaver.pupil.util.Histories
-import xyz.quaver.pupil.util.updateOldReaderGalleries
import java.io.File
class Pupil : MultiDexApplication() {
lateinit var histories: Histories
- lateinit var downloads: Histories
lateinit var favorites: Histories
init {
@@ -53,6 +49,13 @@ class Pupil : MultiDexApplication() {
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
+ val download = preference.getString("dl_location", null)
+
+ if (download == null) {
+ val default = ContextCompat.getExternalFilesDirs(this, null)[0]
+ preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
+ }
+
try {
ProviderInstaller.installIfNeeded(this)
} catch (e: GooglePlayServicesRepairableException) {
@@ -78,14 +81,6 @@ class Pupil : MultiDexApplication() {
false -> AppCompatDelegate.MODE_NIGHT_NO
})
- CoroutineScope(Dispatchers.IO).launch {
- try {
- updateOldReaderGalleries(this@Pupil)
- } catch (e: Exception) {
- // do nothing
- }
- }
-
super.onCreate()
}
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt
index 67f812e4..d7dcdec2 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/GalleryBlockAdapter.kt
@@ -21,7 +21,6 @@ package xyz.quaver.pupil.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Base64
-import android.util.Log
import android.util.SparseBooleanArray
import android.view.LayoutInflater
import android.view.View
@@ -76,8 +75,6 @@ class GalleryBlockAdapter(context: Context, private val galleries: List
- file.nameWithoutExtension.toIntOrNull() != null
- }?.size ?: 0
+ progress = cache?.listFiles()?.count { file ->
+ Regex("^[0-9]+.+\$").matches(file.name!!)
+ } ?: 0
if (visibility == View.GONE) {
visibility = View.VISIBLE
@@ -151,9 +148,9 @@ class GalleryBlockAdapter(context: Context, private val galleries: List
- file.nameWithoutExtension.toIntOrNull() != null
- }?.size ?: 0
+ val count = cache.listFiles().count {
+ Regex("^[0-9]+.+\$").matches(it.name!!)
+ }
with(galleryblock_progressbar) {
max = reader.galleryInfo.size
diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
index 0d5f3dfc..4d1bef48 100644
--- a/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
+++ b/app/src/main/java/xyz/quaver/pupil/adapters/ReaderAdapter.kt
@@ -92,9 +92,10 @@ class ReaderAdapter(private val context: Context,
holder.view.reader_index.text = (position+1).toString()
val images = Cache(context).getImages(galleryID)
+
if (images?.get(position) != null) {
glide
- .load(images[position])
+ .load(images[position]?.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.drawable.image_broken_variant)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index f0111bba..e3610c88 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -54,7 +54,10 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import kotlinx.serialization.stringify
-import xyz.quaver.hitomi.*
+import xyz.quaver.hitomi.GalleryBlock
+import xyz.quaver.hitomi.doSearch
+import xyz.quaver.hitomi.getGalleryIDsFromNozomi
+import xyz.quaver.hitomi.getSuggestionsForQuery
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.adapters.GalleryBlockAdapter
@@ -965,10 +968,10 @@ class MainActivity : AppCompatActivity() {
}
}
Mode.DOWNLOAD -> {
- val downloads = getDownloadDirectory(this@MainActivity).listFiles { file ->
- file.isDirectory and (file.name.toIntOrNull() != null) and File(file, ".metadata").exists()
+ val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
+ file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
}?.map {
- it.name.toInt()
+ it.name!!.toInt()
}?: listOf()
when {
@@ -1020,28 +1023,7 @@ class MainActivity : AppCompatActivity() {
for (chunk in chunks)
chunk.map { galleryID ->
async {
- try {
- val json = Json(JsonConfiguration.Stable)
- val serializer = GalleryBlock.serializer()
-
- File(getCachedGallery(this@MainActivity, galleryID), "galleryBlock.json").let { cache ->
- when {
- cache.exists() -> json.parse(serializer, cache.readText())
- else -> {
- getGalleryBlock(galleryID).apply {
- this ?: return@apply
-
- if (cache.parentFile?.exists() == false)
- cache.parentFile!!.mkdirs()
-
- cache.writeText(json.stringify(serializer, this))
- }
- }
- }
- } ?: return@async null
- } catch (e: Exception) {
- null
- }
+ Cache(this@MainActivity).getGalleryBlock(galleryID)
}
}.forEach {
val galleryBlock = it.await()
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt
index e806513b..617650d9 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt
@@ -20,26 +20,33 @@ package xyz.quaver.pupil.ui
import android.app.Activity
import android.content.Intent
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.view.MenuItem
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
+import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.serialization.ImplicitReflectionSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.parseList
+import net.rdrei.android.dirchooser.DirectoryChooserActivity
import xyz.quaver.pupil.Pupil
import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.LockFragment
import xyz.quaver.pupil.ui.fragment.SettingsFragment
+import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
+import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
+import xyz.quaver.pupil.util.REQUEST_LOCK
+import xyz.quaver.pupil.util.REQUEST_RESTORE
+import java.io.File
import java.nio.charset.Charset
class SettingsActivity : AppCompatActivity() {
- val REQUEST_LOCK = 38238
- val REQUEST_RESTORE = 16546
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -114,6 +121,35 @@ class SettingsActivity : AppCompatActivity() {
}
}
}
+ REQUEST_DOWNLOAD_FOLDER -> {
+ if (resultCode == Activity.RESULT_OK) {
+ data?.data?.also { uri ->
+ val takeFlags: Int = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
+ contentResolver.takePersistableUriPermission(uri, takeFlags)
+
+ if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
+ Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
+ else
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putString("dl_location", uri.toString())
+ .apply()
+ }
+ }
+ }
+ REQUEST_DOWNLOAD_FOLDER_OLD -> {
+ if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
+ val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
+
+ if (!File(directory).canWrite())
+ Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
+ else
+ PreferenceManager.getDefaultSharedPreferences(this).edit()
+ .putString("dl_location", Uri.fromFile(File(directory)).toString())
+ .apply()
+ }
+ }
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialog.kt
index 30854000..d6c2b8b1 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DownloadLocationDialog.kt
@@ -19,8 +19,11 @@
package xyz.quaver.pupil.ui.dialog
import android.annotation.SuppressLint
+import android.app.Activity
import android.app.Dialog
-import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.RadioButton
@@ -28,20 +31,25 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.*
+import net.rdrei.android.dirchooser.DirectoryChooserActivity
+import net.rdrei.android.dirchooser.DirectoryChooserConfig
import xyz.quaver.pupil.R
+import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
+import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
import xyz.quaver.pupil.util.byteToString
@SuppressLint("InflateParams")
-class DownloadLocationDialog(context: Context) : AlertDialog(context) {
+class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
- private val buttons = mutableListOf()
- var onDownloadLocationChangedListener : ((Int) -> (Unit))? = null
+ private val buttons = mutableListOf>()
override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
- ContextCompat.getExternalFilesDirs(context, null).forEachIndexed { index, dir ->
+ val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null)
+
+ externalFilesDirs.forEachIndexed { index, dir ->
dir ?: return@forEachIndexed
@@ -55,17 +63,58 @@ class DownloadLocationDialog(context: Context) : AlertDialog(context) {
byteToString(dir.freeSpace)
)
setOnClickListener {
- buttons.forEach { button ->
- button.isChecked = false
+ buttons.forEach { pair ->
+ pair.first.isChecked = false
}
button.performClick()
- onDownloadLocationChangedListener?.invoke(index)
+ preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
}
- buttons.add(button)
+ buttons.add(button to Uri.fromFile(dir))
})
}
- buttons[preference.getInt("dl_location", 0)].isChecked = true
+ view.addView(layoutInflater.inflate(R.layout.item_dl_location, view, false).apply {
+ location_type.text = context.getString(R.string.settings_dl_location_custom)
+ setOnClickListener {
+ buttons.forEach { pair ->
+ pair.first.isChecked = false
+ }
+ button.performClick()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
+ putExtra("android.content.extra.SHOW_ADVANCED", true)
+ }
+
+ activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
+
+ dismiss()
+ } else { // Can't use SAF on old Androids!
+ val config = DirectoryChooserConfig.builder()
+ .newDirectoryName("Pupil")
+ .allowNewDirectoryNameModification(true)
+ .build()
+
+ val intent = Intent(context, DirectoryChooserActivity::class.java).apply {
+ putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
+ }
+
+ activity.startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER_OLD)
+ dismiss()
+ }
+ }
+ buttons.add(button to null)
+ })
+
+ val pref = Uri.parse(preference.getString("dl_location", null))
+ val index = externalFilesDirs.indexOfFirst {
+ Uri.fromFile(it).toString() == pref.toString()
+ }
+
+ if (index < 0)
+ buttons.last().first.isChecked = true
+ else
+ buttons[index].first.isChecked = true
setTitle(R.string.settings_dl_location)
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
index 2269b18c..bbb31331 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt
@@ -19,10 +19,12 @@
package xyz.quaver.pupil.ui.fragment
import android.content.Intent
+import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
@@ -42,7 +44,14 @@ import java.io.File
class SettingsFragment :
PreferenceFragmentCompat(),
Preference.OnPreferenceClickListener,
- Preference.OnPreferenceChangeListener {
+ Preference.OnPreferenceChangeListener,
+ SharedPreferences.OnSharedPreferenceChangeListener {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this)
+ }
override fun onResume() {
super.onResume()
@@ -62,7 +71,7 @@ class SettingsFragment :
}
}
- private fun getDirSize(dir: File) : String {
+ private fun getDirSize(dir: DocumentFile) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
@@ -77,7 +86,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true)
}
"delete_cache" -> {
- val dir = File(context.cacheDir, "imageCache")
+ val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -92,7 +101,7 @@ class SettingsFragment :
}.show()
}
"delete_downloads" -> {
- val dir = getDownloadDirectory(context)
+ val dir = getDownloadDirectory(context)!!
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -101,10 +110,6 @@ class SettingsFragment :
if (dir.exists())
dir.deleteRecursively()
- val downloads = (activity!!.application as Pupil).downloads
-
- downloads.clear()
-
summary = getDirSize(dir)
}
setNegativeButton(android.R.string.no) { _, _ -> }
@@ -124,14 +129,7 @@ class SettingsFragment :
}.show()
}
"dl_location" -> {
- DownloadLocationDialog(context).apply {
- onDownloadLocationChangedListener = { value ->
- PreferenceManager.getDefaultSharedPreferences(context).edit()
- .putInt(key, value)
- .apply()
- summary = getDownloadDirectory(context).absolutePath
- }
- }.show()
+ DownloadLocationDialog(activity!!).show()
}
"default_query" -> {
DefaultQueryDialog(context).apply {
@@ -143,7 +141,7 @@ class SettingsFragment :
}
"app_lock" -> {
val intent = Intent(context, LockActivity::class.java)
- activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
+ activity?.startActivityForResult(intent, REQUEST_LOCK)
}
"mirrors" -> {
MirrorDialog(context)
@@ -151,8 +149,8 @@ class SettingsFragment :
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
- File(getDownloadDirectory(context), "favorites.json"),
- true
+ context,
+ getDownloadDirectory(context)?.createFile("null", "favorites.json")!!
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -164,7 +162,7 @@ class SettingsFragment :
type = "*/*"
}
- activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_RESTORE)
+ activity?.startActivityForResult(intent, REQUEST_RESTORE)
}
else -> return false
}
@@ -191,6 +189,15 @@ class SettingsFragment :
return true
}
+ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+ when (key) {
+ "dl_location" -> {
+ findPreference(key)?.summary =
+ FileUtils.getPath(context, getDownloadDirectory(context!!)?.uri)
+ }
+ }
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
@@ -217,13 +224,13 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
- val dir = File(context.cacheDir, "imageCache")
+ val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
}
"delete_downloads" -> {
- val dir = getDownloadDirectory(context)
+ val dir = getDownloadDirectory(context)!!
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
@@ -235,7 +242,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
- summary = getDownloadDirectory(context).absolutePath
+ summary = FileUtils.getPath(context, getDownloadDirectory(context)?.uri)
onPreferenceClickListener = this@SettingsFragment
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt b/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
new file mode 100644
index 00000000..807c1379
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
@@ -0,0 +1,24 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2020 tom5079
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package xyz.quaver.pupil.util
+
+const val REQUEST_LOCK = 38238
+const val REQUEST_RESTORE = 16546
+const val REQUEST_DOWNLOAD_FOLDER = 3874
+const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/FileUtils.java b/app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
new file mode 100644
index 00000000..414da1c8
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
@@ -0,0 +1,178 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2020 tom5079
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package xyz.quaver.pupil.util;
+
+/*
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * 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.
+ */
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+
+/**
+ * @version 2009-07-03
+ * @author Peli
+ * @version 2013-12-11
+ * @author paulburke (ipaulpro)
+ */
+public class FileUtils {
+ /**
+ * Get a file path from a Uri. This will get the the path for Storage Access
+ * Framework Documents, as well as the _data field for the MediaStore and
+ * other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @author paulburke
+ */
+ public static String getPath(final Context context, final Uri uri) {
+
+ // DocumentProvider
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return context.getExternalFilesDir(null).getParentFile().getParentFile().getParentFile().getParent() + "/" + split[1];
+ }
+
+ // TODO handle non-primary volumes
+ }
+ // DownloadsProvider
+ else if (isDownloadsDocument(uri)) {
+
+ final String id = DocumentsContract.getDocumentId(uri);
+ final Uri contentUri = ContentUris.withAppendedId(
+ Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+ return getDataColumn(context, contentUri, null, null);
+ }
+ // MediaProvider
+ else if (isMediaDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ Uri contentUri = null;
+ if ("image".equals(type)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if ("video".equals(type)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else if ("audio".equals(type)) {
+ contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+ }
+
+ final String selection = "_id=?";
+ final String[] selectionArgs = new String[] {
+ split[1]
+ };
+
+ return getDataColumn(context, contentUri, selection, selectionArgs);
+ }
+ }
+ // MediaStore (and general)
+ else if ("content".equalsIgnoreCase(uri.getScheme())) {
+ return getDataColumn(context, uri, null, null);
+ }
+ // File
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the value of the data column for this Uri. This is useful for
+ * MediaStore Uris, and other file-based ContentProviders.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @param selection (Optional) Filter used in the query.
+ * @param selectionArgs (Optional) Selection arguments used in the query.
+ * @return The value of the _data column, which is typically a file path.
+ */
+ public static String getDataColumn(Context context, Uri uri, String selection,
+ String[] selectionArgs) {
+
+ Cursor cursor = null;
+ final String column = "_data";
+ final String[] projection = {
+ column
+ };
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final int column_index = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(column_index);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
+
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ public static boolean isExternalStorageDocument(Uri uri) {
+ return "com.android.externalstorage.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is DownloadsProvider.
+ */
+ public static boolean isDownloadsDocument(Uri uri) {
+ return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+ }
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is MediaProvider.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt b/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt
deleted file mode 100644
index c038622a..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/GalleryDownloader.kt
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package xyz.quaver.pupil.util
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.Intent
-import android.util.SparseArray
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.app.TaskStackBuilder
-import androidx.preference.PreferenceManager
-import com.crashlytics.android.Crashlytics
-import kotlinx.coroutines.*
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonConfiguration
-import xyz.quaver.hitomi.Reader
-import xyz.quaver.hitomi.getReader
-import xyz.quaver.hitomi.getReferer
-import xyz.quaver.hitomi.urlFromUrlFromHash
-import xyz.quaver.hiyobi.cookie
-import xyz.quaver.hiyobi.createImgList
-import xyz.quaver.hiyobi.user_agent
-import xyz.quaver.pupil.Pupil
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.ui.ReaderActivity
-import java.io.File
-import java.io.FileOutputStream
-import java.net.URL
-import java.util.*
-import javax.net.ssl.HttpsURLConnection
-import kotlin.collections.ArrayList
-import kotlin.concurrent.schedule
-
-@Deprecated("Use DownloadWorker instead")
-class GalleryDownloader(
- base: Context,
- private val galleryID: Int,
- _notify: Boolean = false
-) : ContextWrapper(base) {
-
- private val downloads = (applicationContext as Pupil).downloads
- var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
-
- var download: Boolean = false
- set(value) {
- if (value) {
- field = true
- notificationManager.notify(galleryID, notificationBuilder.build())
-
- if (reader?.isActive == false && downloadJob?.isActive != true) {
- val data = File(
- getDownloadDirectory(
- this
- ), galleryID.toString())
- val cache = File(cacheDir, "imageCache/$galleryID")
-
- if (File(cache, "images").exists() && !data.exists()) {
- cache.copyRecursively(data, true)
- cache.deleteRecursively()
- }
-
- field = false
- }
-
- downloads.add(galleryID)
- } else {
- field = false
- }
-
- onNotifyChangedHandler?.invoke(value)
- }
-
- val reader: Deferred?
- private var downloadJob: Job? = null
-
- private lateinit var notificationBuilder: NotificationCompat.Builder
- private lateinit var notificationManager: NotificationManagerCompat
-
- var onReaderLoadedHandler: ((Reader) -> Unit)? = null
- var onProgressHandler: ((Int) -> Unit)? = null
- var onDownloadedHandler: ((List) -> Unit)? = null
- var onErrorHandler: ((Exception) -> Unit)? = null
- var onCompleteHandler: (() -> Unit)? = null
- var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
-
- companion object : SparseArray()
-
- init {
- put(galleryID, this)
-
- initNotification()
-
- reader = CoroutineScope(Dispatchers.IO).async {
- try {
- download = _notify
- val json = Json(JsonConfiguration.Stable)
- val serializer = Reader.serializer()
-
- //Check cache
- val cache = File(
- getCachedGallery(
- this@GalleryDownloader,
- galleryID
- ), "reader.json")
-
- try {
- json.parse(serializer, cache.readText())
- } catch(e: Exception) {
- cache.delete()
- }
-
- if (cache.exists()) {
- val cached = json.parse(serializer, cache.readText())
-
- if (cached.galleryInfo.isNotEmpty()) {
- useHiyobi = cached.code == Reader.Code.HIYOBI
-
- onReaderLoadedHandler?.invoke(cached)
-
- return@async cached
- }
- }
-
- //Cache doesn't exist. Load from internet
- val reader = when {
- useHiyobi -> {
- try {
- xyz.quaver.hiyobi.getReader(galleryID)
- } catch(e: Exception) {
- useHiyobi = false
- getReader(galleryID)
- }
- }
- else -> {
- getReader(galleryID)
- }
- }
-
- if (reader.galleryInfo.isNotEmpty()) {
- //Save cache
- if (cache.parentFile?.exists() == false)
- cache.parentFile!!.mkdirs()
-
- cache.writeText(json.stringify(serializer, reader))
- }
-
- reader
- } catch (e: Exception) {
- Crashlytics.logException(e)
- onErrorHandler?.invoke(e)
- null
- }
- }
- }
-
- fun start() {
- downloadJob = CoroutineScope(Dispatchers.Default).launch {
- val reader = reader!!.await() ?: return@launch
- val lowQuality = PreferenceManager.getDefaultSharedPreferences(this@GalleryDownloader)
- .getBoolean("low_quality", false)
-
- notificationBuilder.setContentTitle(reader.title)
-
- val list = ArrayList()
-
- onReaderLoadedHandler?.invoke(reader)
-
- notificationBuilder
- .setProgress(reader.galleryInfo.size, 0, false)
- .setContentText("0/${reader.galleryInfo.size}")
-
- reader.galleryInfo.chunked(4).forEachIndexed { chunkIndex, chunk ->
- chunk.mapIndexed { i, galleryInfo ->
- val index = chunkIndex*4+i
-
- async(Dispatchers.IO) {
- val url = when(useHiyobi) {
- true -> createImgList(galleryID, reader, lowQuality)[index].path
- false -> when {
- (!galleryInfo.hash.isNullOrBlank()) && (galleryInfo.haswebp == 1) && lowQuality ->
- urlFromUrlFromHash(galleryID, galleryInfo, "webp")
- else ->
- urlFromUrlFromHash(galleryID, galleryInfo)
- }
- }
-
- val name = "$index".padStart(4, '0')
- val ext = url.split('.').last()
-
- val cache = File(
- getCachedGallery(
- this@GalleryDownloader,
- galleryID
- ), "images/$name.$ext")
-
- if (!cache.exists())
- try {
- with(URL(url).openConnection() as HttpsURLConnection) {
- if (useHiyobi) {
- setRequestProperty("User-Agent", user_agent)
- setRequestProperty("Cookie", cookie)
- } else
- setRequestProperty("Referer", getReferer(galleryID))
-
- if (cache.parentFile?.exists() == false)
- cache.parentFile!!.mkdirs()
-
- inputStream.copyTo(FileOutputStream(cache))
- }
- } catch (e: Exception) {
- cache.delete()
-
- onErrorHandler?.invoke(e)
-
- notificationBuilder
- .setContentTitle(reader.title)
- .setContentText(getString(R.string.reader_notification_error))
- .setProgress(0, 0, false)
-
- notificationManager.notify(galleryID, notificationBuilder.build())
- }
-
- "images/$name.$ext"
- }
- }.forEach {
- list.add(it.await())
-
- val index = list.size
-
- onProgressHandler?.invoke(index)
-
- notificationBuilder
- .setProgress(reader.galleryInfo.size, index, false)
- .setContentText("$index/${reader.galleryInfo.size}")
-
- if (download)
- notificationManager.notify(galleryID, notificationBuilder.build())
-
- onDownloadedHandler?.invoke(list)
- }
- }
-
- Timer(false).schedule(1000) {
- notificationBuilder
- .setContentTitle(reader.title)
- .setContentText(getString(R.string.reader_notification_complete))
- .setProgress(0, 0, false)
-
- if (download) {
- File(cacheDir, "imageCache/${galleryID}").let {
- if (it.exists()) {
- val target = File(
- getDownloadDirectory(
- this@GalleryDownloader
- ), galleryID.toString())
-
- if (!target.exists())
- target.mkdirs()
-
- it.copyRecursively(target, true)
- it.deleteRecursively()
- }
- }
-
- notificationManager.notify(galleryID, notificationBuilder.build())
-
- download = false
- }
-
- onCompleteHandler?.invoke()
- }
-
- remove(galleryID)
- }
- }
-
- fun cancel() {
- downloadJob?.cancel()
-
- remove(galleryID)
- }
-
- suspend fun cancelAndJoin() {
- downloadJob?.cancelAndJoin()
-
- remove(galleryID)
- }
-
- fun invokeOnReaderLoaded() {
- CoroutineScope(Dispatchers.Default).launch {
- onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
- }
- }
-
- fun clearNotification() {
- notificationManager.cancel(galleryID)
- }
-
- fun invokeOnNotifyChanged() {
- onNotifyChangedHandler?.invoke(download)
- }
-
- private fun initNotification() {
- val intent = Intent(this, ReaderActivity::class.java).apply {
- putExtra("galleryID", galleryID)
- }
- val pendingIntent = TaskStackBuilder.create(this).run {
- addNextIntentWithParentStack(intent)
- getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
- }
-
- notificationManager = NotificationManagerCompat.from(this)
-
- notificationBuilder = NotificationCompat.Builder(this, "download").apply {
- setContentTitle(getString(R.string.reader_loading))
- setContentText(getString(R.string.reader_notification_text))
- setSmallIcon(android.R.drawable.stat_sys_download)
- setContentIntent(pendingIntent)
- setProgress(0, 0, true)
- priority = NotificationCompat.PRIORITY_LOW
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
index 93a6ec73..85baed6c 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
@@ -21,7 +21,7 @@ package xyz.quaver.pupil.util.download
import android.content.Context
import android.content.ContextWrapper
import android.util.Base64
-import androidx.core.content.ContextCompat
+import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import kotlinx.coroutines.*
import kotlinx.serialization.ImplicitReflectionSerializer
@@ -30,8 +30,7 @@ import kotlinx.serialization.parse
import kotlinx.serialization.stringify
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
-import xyz.quaver.pupil.util.getDownloadDirectory
-import xyz.quaver.pupil.util.isParentOf
+import xyz.quaver.pupil.util.*
import java.io.File
import java.net.URL
@@ -41,17 +40,13 @@ class Cache(context: Context) : ContextWrapper(context) {
// Search in this order
// Download -> Cache
- fun getCachedGallery(galleryID: Int) : File? {
- var file : File
+ fun getCachedGallery(galleryID: Int) : DocumentFile? {
+ var file = getDownloadDirectory(this)?.findFile(galleryID.toString())
- ContextCompat.getExternalFilesDirs(this, null).forEach {
- file = File(it, galleryID.toString())
+ if (file?.exists() == true)
+ return file
- if (file.exists())
- return file
- }
-
- file = File(cacheDir, "imageCache/$galleryID")
+ file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
return if (file.exists())
file
@@ -61,13 +56,13 @@ class Cache(context: Context) : ContextWrapper(context) {
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
- val file = File(getCachedGallery(galleryID) ?: return null, ".metadata")
+ val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
- if (!file.exists())
+ if (file?.exists() != true)
return null
return try {
- Json.parse(file.readText())
+ Json.parse(file.readText(this))
} catch (e: Exception) {
//File corrupted
file.delete()
@@ -77,14 +72,13 @@ class Cache(context: Context) : ContextWrapper(context) {
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
- val file = File(getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID"), ".metadata")
+ val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
+ DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
+ if (!it.exists())
+ it.mkdirs()
+ }).createFile("null", ".metadata") ?: return
- if (file.parentFile?.exists() != true)
- file.parentFile?.mkdirs()
-
- file.createNewFile()
-
- file.writeText(Json.stringify(metadata))
+ file.writeText(this, Json.stringify(metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
@@ -139,7 +133,7 @@ class Cache(context: Context) : ContextWrapper(context) {
return metadata?.readers?.firstOrNull {
mirrors.contains(it.code.name)
- }
+ } ?: metadata?.readers?.firstOrNull()
}
suspend fun getReader(galleryID: Int): Reader? {
@@ -170,49 +164,44 @@ class Cache(context: Context) : ContextWrapper(context) {
return readers.firstOrNull {
mirrors.contains(it.code.name)
- }
+ } ?: readers.firstOrNull()
}
- fun getImages(galleryID: Int): List? {
+ fun getImages(galleryID: Int): List? {
val gallery = getCachedGallery(galleryID) ?: return null
val reader = getReaderOrNull(galleryID) ?: return null
- val images = gallery.listFiles() ?: return null
+ val images = gallery.listFiles()
return reader.galleryInfo.indices.map { index ->
- images.firstOrNull { file -> file.nameWithoutExtension.toIntOrNull() == index }
+ images.firstOrNull { file -> file.name?.startsWith(index.toString()) == true }
}
}
fun putImage(galleryID: Int, name: String, data: ByteArray) {
- val cache = getCachedGallery(galleryID) ?: File(cacheDir, "imageCache/$galleryID")
+ val cache = getCachedGallery(galleryID) ?:
+ DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
+ if (!it.exists())
+ it.mkdirs()
+ }) ?: return
- with(File(cache, name)) {
+ if (!Regex("""^[0-9]+.+$""").matches(name))
+ throw IllegalArgumentException("File name is not a number")
- if (!parentFile!!.exists())
- parentFile!!.mkdirs()
-
- if (!exists())
- createNewFile()
-
- if (nameWithoutExtension.toIntOrNull() != null)
- writeBytes(data)
- else
- IllegalArgumentException("File name is not a number")
- }
+ cache.createFile("null", name)?.writeBytes(this, data)
}
fun moveToDownload(galleryID: Int) {
val cache = getCachedGallery(galleryID)
if (cache != null) {
- val download = File(getDownloadDirectory(this), galleryID.toString())
+ val download = getDownloadDirectory(this)!!
if (!download.isParentOf(cache)) {
- cache.copyRecursively(download, true)
+ cache.copyRecursively(this, download)
cache.deleteRecursively()
}
} else
- File(getDownloadDirectory(this), galleryID.toString()).mkdirs()
+ getDownloadDirectory(this)?.createDirectory(galleryID.toString())
}
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
diff --git a/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt b/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt
index b9bd9a46..5ec60ba9 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/download/DownloadWorker.kt
@@ -335,14 +335,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
if (isCompleted(galleryID))
notification[galleryID]
- .setContentText(getString(R.string.reader_notification_complete))
- .setProgress(0, 0, false)
+ ?.setContentText(getString(R.string.reader_notification_complete))
+ ?.setProgress(0, 0, false)
else
notification[galleryID]
- .setProgress(max, progress, false)
- .setContentText("$progress/$max")
+ ?.setProgress(max, progress, false)
+ ?.setContentText("$progress/$max")
- if (Cache(this).isDownloading(galleryID))
+ if (Cache(this).isDownloading(galleryID) && notification[galleryID] != null)
notificationManager.notify(galleryID, notification[galleryID].build())
else
notificationManager.cancel(galleryID)
diff --git a/app/src/main/java/xyz/quaver/pupil/util/file.kt b/app/src/main/java/xyz/quaver/pupil/util/file.kt
index dde5e9e4..bf952028 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/file.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/file.kt
@@ -19,28 +19,32 @@
package xyz.quaver.pupil.util
import android.content.Context
-import androidx.core.content.ContextCompat
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import java.io.File
import java.net.URL
+import java.nio.charset.Charset
+import java.util.*
-fun getCachedGallery(context: Context, galleryID: Int): File {
- return File(getDownloadDirectory(context), galleryID.toString()).let {
- when {
- it.exists() -> it
- else -> File(context.cacheDir, "imageCache/$galleryID")
- }
+fun getCachedGallery(context: Context, galleryID: Int) =
+ getDownloadDirectory(context)?.findFile(galleryID.toString()) ?:
+ DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
+
+fun getDownloadDirectory(context: Context) : DocumentFile? {
+ val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
+ Uri.parse(it)
}
+
+ return if (uri.toString().startsWith("file"))
+ DocumentFile.fromFile(File(uri.path!!))
+ else
+ DocumentFile.fromTreeUri(context, uri)
}
-fun getDownloadDirectory(context: Context): File {
- val dlLocation = PreferenceManager.getDefaultSharedPreferences(context).getInt("dl_location", 0)
-
- return ContextCompat.getExternalFilesDirs(context, null)[dlLocation]
-}
-
-fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
- to.outputStream().use { out ->
+fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
+ context.contentResolver.openOutputStream(to.uri).use { out ->
+ out!!
with(openConnection()) {
val fileSize = contentLength.toLong()
@@ -64,4 +68,69 @@ fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
}
}
-fun File.isParentOf(file: File?) = file?.absolutePath?.startsWith(this.absolutePath) ?: false
\ No newline at end of file
+fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
+ var parent = file?.parentFile
+ while (parent != null) {
+ if (this.uri.path == parent.uri.path)
+ return true
+
+ parent = parent.parentFile
+ }
+
+ return false
+}
+
+fun DocumentFile.reader(context: Context, charset: Charset = Charsets.UTF_8) = context.contentResolver.openInputStream(uri)!!.reader(charset)
+fun DocumentFile.readBytes(context: Context) = context.contentResolver.openInputStream(uri)!!.readBytes()
+fun DocumentFile.readText(context: Context, charset: Charset = Charsets.UTF_8) = reader(context, charset).use { it.readText() }
+
+fun DocumentFile.writeBytes(context: Context, array: ByteArray) = context.contentResolver.openOutputStream(uri)!!.write(array)
+fun DocumentFile.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8) = writeBytes(context, text.toByteArray(charset))
+
+fun DocumentFile.copyRecursively(
+ context: Context,
+ target: DocumentFile
+) {
+ if (!exists())
+ throw Exception("The source file doesn't exist.")
+
+ if (this.isFile)
+ target.createFile("null", name!!)!!.writeBytes(
+ context,
+ readBytes(context)
+ )
+ else if (this.isDirectory) {
+ target.createDirectory(name!!).also { newTarget ->
+ listFiles().forEach { child ->
+ child.copyRecursively(context, newTarget!!)
+ }
+ }
+ }
+}
+
+fun DocumentFile.deleteRecursively() {
+
+ if (this.isDirectory)
+ listFiles().forEach {
+ it.deleteRecursively()
+ }
+
+ this.delete()
+}
+
+fun DocumentFile.walk(state: LinkedList = LinkedList()) : Queue {
+ if (state.isEmpty())
+ state.push(this)
+
+ listFiles().forEach {
+ state.push(it)
+
+ if (it.isDirectory) {
+ it.walk(state)
+ }
+ }
+
+ return state
+}
+
+fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/update.kt b/app/src/main/java/xyz/quaver/pupil/util/update.kt
index a95f7b0e..c75f4dac 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/update.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt
@@ -21,27 +21,20 @@ package xyz.quaver.pupil.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import androidx.core.content.FileProvider
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.serialization.InternalSerializationApi
-import kotlinx.serialization.internal.EnumSerializer
import kotlinx.serialization.json.*
import ru.noties.markwon.Markwon
-import xyz.quaver.availableInHiyobi
-import xyz.quaver.hitomi.Reader
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
-import java.io.File
import java.net.URL
import java.util.*
@@ -153,10 +146,10 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
}
CoroutineScope(Dispatchers.IO).launch io@{
- val target = File(getDownloadDirectory(context), "Pupil.apk")
+ val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
try {
- URL(url).download(target) { progress, fileSize ->
+ URL(url).download(context, target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
@@ -175,15 +168,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
val install = Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
- setDataAndType(FileProvider.getUriForFile(
- context,
- context.applicationContext.packageName + ".fileprovider",
- target
- ), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
-
- if (resolveActivity(context.packageManager) == null)
- setDataAndType(Uri.fromFile(target),
- MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
+ setDataAndType(target.uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}
builder.apply {
@@ -214,59 +199,4 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
dialog.show()
}
}
-}
-
-fun getOldReaderGalleries(context: Context) : List {
- val oldGallery = mutableListOf()
-
- listOf(
- getDownloadDirectory(context),
- File(context.cacheDir, "imageCache")
- ).forEach { root ->
- root.listFiles()?.forEach { gallery ->
- File(gallery, "reader.json").let { readerFile ->
- if (!readerFile.exists())
- return@let
-
- try {
- Json(JsonConfiguration.Stable).parseJson(readerFile.readText())
- .jsonObject.let { reader ->
- if (!reader.contains("code"))
- oldGallery.add(gallery)
- }
- } catch (e: Exception) {
- // do nothing
- }
- }
- }
- }
-
- return oldGallery
-}
-
-@UseExperimental(InternalSerializationApi::class)
-fun updateOldReaderGalleries(context: Context) {
-
- val json = Json(JsonConfiguration.Stable)
-
- getOldReaderGalleries(context).forEach { gallery ->
- val reader = json.parseJson(File(gallery, "reader.json").apply {
- if (!exists())
- return@forEach
- }.readText())
- .jsonObject.toMutableMap()
-
- val codeSerializer = EnumSerializer(Reader.Code::class)
-
- reader["code"] = when {
- (File(gallery, "images").list()?.
- all { !it.endsWith("webp") } ?: return@forEach) &&
- availableInHiyobi(gallery.name.toIntOrNull() ?: return@forEach)
- -> json.toJson(codeSerializer, Reader.Code.HIYOBI)
- else -> json.toJson(codeSerializer, Reader.Code.HITOMI)
- }
-
- File(gallery, "reader.json").writeText(JsonObject(reader).toString())
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 3dc4bf7c..39a0af29 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -120,4 +120,6 @@
v%s
低解像度イメージ
ロード速度とデータ使用料を改善するため低解像度イメージをロード
+ 手動で設定
+ このフォルダにアクセスできません。他のフォルダを選択してください。
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 34287193..e5829cb9 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -120,4 +120,6 @@
로드 속도와 데이터 사용량을 줄이기 위해 저해상도 이미지를 로드
미러 서버에서 이미지 로드
미러 설정
+ 직접 설정
+ 이 폴더에 접근할 수 없습니다. 다른 폴더를 선택해주세요.
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0640b664..e7e389b8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -140,6 +140,8 @@
Removable Storage
Internal Storage
%s available
+ Custom Location
+ This folder is not writable. Please select another folder.
Low quality images
Load low quality images to improve load speed and data usage
diff --git a/build.gradle b/build.gradle
index 12b58e00..495dc04c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,7 +15,7 @@ buildscript {
classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
- classpath 'io.fabric.tools:gradle:1.29.0'
+ classpath 'io.fabric.tools:gradle:1.31.0'
classpath 'com.google.firebase:perf-plugin:1.3.1'
}
}
@@ -25,6 +25,7 @@ allprojects {
google()
jcenter()
maven { url "https://jitpack.io" }
+ maven { url 'http://guardian.github.com/maven/repo-releases' }
}
}
diff --git a/libpupil/build.gradle b/libpupil/build.gradle
index 2f748f9e..96abc57b 100644
--- a/libpupil/build.gradle
+++ b/libpupil/build.gradle
@@ -5,10 +5,10 @@ apply plugin: 'kotlinx-serialization'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
implementation 'org.jsoup:jsoup:1.12.1'
- testImplementation 'junit:junit:4.12'
+ testImplementation 'junit:junit:4.13'
}
sourceCompatibility = "7"