diff --git a/app/build.gradle b/app/build.gradle
index 5df53c31..89f7dced 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,13 +14,13 @@ if (file("google-services.json").exists()) {
}
android {
- compileSdkVersion 29
+ compileSdkVersion 28
defaultConfig {
applicationId "xyz.quaver.pupil"
minSdkVersion 16
- targetSdkVersion 29
+ targetSdkVersion 28
versionCode 37
- versionName "5.3-beta4"
+ versionName "5.3-hotfix1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
@@ -60,9 +60,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
- implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'com.android.support:multidex:1.0.3'
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
@@ -72,7 +69,8 @@ dependencies {
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
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:glide:4.11.0'
+ kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
transitive = false
}
@@ -81,7 +79,6 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
implementation "ru.noties.markwon:core:${markwonVersion}"
- kapt 'com.github.bumptech.glide:compiler:4.10.0'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0'
diff --git a/app/release/output.json b/app/release/output.json
index 88f6ce43..cc3bb359 100644
--- a/app/release/output.json
+++ b/app/release/output.json
@@ -1 +1 @@
-[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":37,"versionName":"5.3-beta4","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":37,"versionName":"5.3-hotfix1","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/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt
index 680e9e3a..92c22553 100644
--- a/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/xyz/quaver/pupil/ExampleInstrumentedTest.kt
@@ -26,11 +26,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.ImplicitReflectionSerializer
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonConfiguration
-import kotlinx.serialization.json.JsonObject
-import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import xyz.quaver.hiyobi.cookie
@@ -40,9 +35,6 @@ import xyz.quaver.hiyobi.user_agent
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.util.download.Cache
import xyz.quaver.pupil.util.download.DownloadWorker
-import xyz.quaver.pupil.util.getDownloadDirectory
-import xyz.quaver.pupil.util.updateOldReaderGalleries
-import java.io.File
import java.net.URL
import javax.net.ssl.HttpsURLConnection
@@ -58,8 +50,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
- assertEquals("xyz.quaver.pupil", appContext.packageName)
}
@Test
@@ -88,40 +78,6 @@ class ExampleInstrumentedTest {
Log.d("Pupil", data.size.toString())
}
- @UseExperimental(ImplicitReflectionSerializer::class)
- @Test
- fun test_deleteCodeFromReader() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
-
- val json = Json(JsonConfiguration.Stable)
-
- listOf(
- getDownloadDirectory(context),
- File(context.cacheDir, "imageCache")
- ).forEach { root ->
- root.listFiles()?.forEach gallery@{ gallery ->
- val reader = json.parseJson(File(gallery, "reader.json").apply {
- if (!exists())
- return@gallery
- }.readText())
- .jsonObject.toMutableMap()
-
- Log.d("PUPILD", gallery.name)
-
- reader.remove("code")
-
- File(gallery, "reader.json").writeText(JsonObject(reader).toString())
- }
- }
- }
-
- @Test
- fun test_updateOldReader() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
-
- updateOldReaderGalleries(context)
- }
-
@Test
fun test_downloadWorker() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 09fe339b..d445644f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,8 +7,7 @@
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- Regex("^[0-9]+.+\$").matches(file.name!!)
- } ?: 0
+ progress = cache.listFiles()?.count { file ->
+ Regex("^[0-9]+.+\$").matches(file.name)
+ } ?: 0
- if (visibility == View.GONE) {
- visibility = View.VISIBLE
- max = reader.galleryInfo.size
- }
+ if (visibility == View.GONE) {
+ visibility = View.VISIBLE
+ max = reader.galleryInfo.size
+ }
- if (progress == max) {
- if (completeFlag.get(galleryID, false)) {
- with(view.galleryblock_progress_complete) {
- setImageResource(R.drawable.ic_progressbar)
- visibility = View.VISIBLE
- }
- } else {
- with(view.galleryblock_progress_complete) {
- setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
- this?.start()
- })
- visibility = View.VISIBLE
+ if (progress == max) {
+ if (completeFlag.get(galleryID, false)) {
+ with(view.galleryblock_progress_complete) {
+ setImageResource(R.drawable.ic_progressbar)
+ visibility = View.VISIBLE
+ }
+ } else {
+ with(view.galleryblock_progress_complete) {
+ setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
+ this?.start()
+ })
+ visibility = View.VISIBLE
+ }
+ completeFlag.put(galleryID, true)
}
- completeFlag.put(galleryID, true)
- }
- } else
- view.galleryblock_progress_complete.visibility = View.INVISIBLE
+ } else
+ view.galleryblock_progress_complete.visibility = View.INVISIBLE
+ }
}
}
@@ -152,10 +154,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List() {
- var isFullScreen = false
+ //region Glide.RecyclerView
+ inner class SizeProvider : ListPreloader.PreloadSizeProvider {
+
+ override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? {
+ return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let {
+ arrayOf(it.width, it.height).toIntArray()
+ }
+ }
+
+ }
+
+ inner class ModelProvider : ListPreloader.PreloadModelProvider {
+
+ override fun getPreloadItems(position: Int): MutableList {
+ return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf())
+ }
+
+ override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? {
+ return glide
+ .load(item)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .skipMemoryCache(true)
+ .error(R.drawable.image_broken_variant)
+ .apply {
+ if (BuildConfig.CENSOR)
+ override(5, 8)
+ }
+ }
+
+ }
+ //endregion
var reader: Reader? = null
- private val glide = Glide.with(context)
+ val glide = Glide.with(context)
val timer = Timer()
+ val sizeProvider = SizeProvider()
+ val modelProvider = ModelProvider()
+ val preloader = RecyclerViewPreloader(glide, modelProvider, sizeProvider, 10)
+
+ var isFullScreen = false
+
var onItemClickListener : ((Int) -> (Unit))? = null
init {
@@ -92,46 +132,47 @@ 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]?.uri)
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .skipMemoryCache(true)
- .error(R.drawable.image_broken_variant)
- .apply {
- if (BuildConfig.CENSOR)
- override(5, 8)
- }
- .into(holder.view.image)
- } else {
- val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
-
- if (progress?.isNaN() == true) {
-
- if (Fabric.isInitialized())
- Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
-
- glide
- .load(R.drawable.image_broken_variant)
- .into(holder.view.image)
-
- return
- }
-
- holder.view.reader_item_progressbar.progress =
- if (progress?.isInfinite() == true)
- 100
- else
- progress?.roundToInt() ?: 0
-
- holder.view.image.setImageDrawable(null)
-
+ CoroutineScope(Dispatchers.IO).launch {
+ val images = Cache(context).getImages(galleryID)
- timer.schedule(1000) {
- CoroutineScope(Dispatchers.Main).launch {
- notifyItemChanged(position)
+ launch(Dispatchers.Main) {
+ if (images?.get(position) != null) {
+ glide
+ .load(images[position])
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .skipMemoryCache(true)
+ .error(R.drawable.image_broken_variant)
+ .apply {
+ if (BuildConfig.CENSOR)
+ override(5, 8)
+ }
+ .into(holder.view.image)
+ } else {
+ val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
+
+ if (progress?.isNaN() == true) {
+
+ if (Fabric.isInitialized())
+ Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
+
+ glide
+ .load(R.drawable.image_broken_variant)
+ .into(holder.view.image)
+ } else {
+ holder.view.reader_item_progressbar.progress =
+ if (progress?.isInfinite() == true)
+ 100
+ else
+ progress?.roundToInt() ?: 0
+
+ holder.view.image.setImageDrawable(null)
+ }
+
+ timer.schedule(1000) {
+ CoroutineScope(Dispatchers.Main).launch {
+ notifyItemChanged(position)
+ }
+ }
}
}
}
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 54df43d2..d5f33096 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -429,12 +429,7 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Default).launch {
DownloadWorker.getInstance(context).cancel(galleryID)
- var cache = Cache(context).getCachedGallery(galleryID)
-
- while (cache != null) {
- cache.deleteRecursively()
- cache = Cache(context).getCachedGallery(galleryID)
- }
+ Cache(context).getCachedGallery(galleryID).deleteRecursively()
histories.remove(galleryID)
@@ -969,11 +964,11 @@ class MainActivity : AppCompatActivity() {
}
}
Mode.DOWNLOAD -> {
- val downloads = getDownloadDirectory(this@MainActivity).listFiles().filter { file ->
- file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
- }.map {
- it.name!!.toInt()
- }
+ val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
+ file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
+ }?.map {
+ it.name.toInt()
+ } ?: emptyList()
when {
query.isEmpty() -> downloads.apply {
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
index ee2f7b3d..7af22aec 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
@@ -239,7 +239,7 @@ class ReaderActivity : AppCompatActivity() {
queue.add(galleryID)
}
- timer.schedule(0, 1000) {
+ timer.schedule(1000, 1000) {
if (worker.progress.indexOfKey(galleryID) < 0) //loading
return@schedule
@@ -296,6 +296,7 @@ class ReaderActivity : AppCompatActivity() {
}
}
+ //addOnScrollListener((adapter as ReaderAdapter).preloader)
addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
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 617650d9..aa09d8a4 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt
@@ -18,15 +18,15 @@
package xyz.quaver.pupil.ui
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
-import android.net.Uri
+import android.content.pm.PackageManager
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.*
@@ -38,10 +38,7 @@ 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 xyz.quaver.pupil.util.*
import java.io.File
import java.nio.charset.Charset
@@ -124,16 +121,23 @@ 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)
+ 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()
+ val file = uri.toFile(this)
+
+ if (file?.canWrite() != true)
+ Snackbar.make(
+ settings,
+ R.string.settings_dl_location_not_writable,
+ Snackbar.LENGTH_LONG
+ ).show()
else
PreferenceManager.getDefaultSharedPreferences(this).edit()
- .putString("dl_location", uri.toString())
+ .putString("dl_location", file.canonicalPath)
.apply()
}
}
@@ -143,14 +147,33 @@ class SettingsActivity : AppCompatActivity() {
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()
+ 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())
+ .putString("dl_location", File(directory).canonicalPath)
.apply()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
+
+ @SuppressLint("InlinedApi")
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ when (requestCode) {
+ REQUEST_WRITE_PERMISSION_AND_SAF -> {
+ if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
+ putExtra("android.content.extra.SHOW_ADVANCED", true)
+ }
+
+ startActivityForResult(intent, REQUEST_DOWNLOAD_FOLDER)
+ }
+ }
+ }
+ }
}
\ No newline at end of file
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 d6c2b8b1..3f24f0b0 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
@@ -18,16 +18,18 @@
package xyz.quaver.pupil.ui.dialog
+import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Intent
-import android.net.Uri
+import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.LinearLayout
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
+import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.item_dl_location.view.*
@@ -36,13 +38,15 @@ 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.REQUEST_WRITE_PERMISSION_AND_SAF
import xyz.quaver.pupil.util.byteToString
+import java.io.File
@SuppressLint("InflateParams")
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
- private val buttons = mutableListOf>()
+ private val buttons = mutableListOf>()
override fun onCreate(savedInstanceState: Bundle?) {
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
@@ -67,9 +71,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
pair.first.isChecked = false
}
button.performClick()
- preference.edit().putString("dl_location", Uri.fromFile(dir).toString()).apply()
+ preference.edit().putString("dl_location", dir.canonicalPath).apply()
}
- buttons.add(button to Uri.fromFile(dir))
+ buttons.add(button to dir)
})
}
@@ -82,11 +86,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
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)
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
+ ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_PERMISSION_AND_SAF)
+ else {
+ 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!
@@ -106,9 +115,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
buttons.add(button to null)
})
- val pref = Uri.parse(preference.getString("dl_location", null))
+ val pref = preference.getString("dl_location", null)
val index = externalFilesDirs.indexOfFirst {
- Uri.fromFile(it).toString() == pref.toString()
+ it.canonicalPath == pref
}
if (index < 0)
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 98e29b6f..f3d88dad 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
@@ -24,7 +24,6 @@ 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
@@ -71,7 +70,7 @@ class SettingsFragment :
}
}
- private fun getDirSize(dir: DocumentFile) : String {
+ private fun getDirSize(dir: File) : String {
val size = dir.walk().map { it.length() }.sum()
return getString(R.string.settings_clear_summary, byteToString(size))
@@ -86,7 +85,7 @@ class SettingsFragment :
checkUpdate(activity as SettingsActivity, true)
}
"delete_cache" -> {
- val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
+ val dir = File(context.cacheDir, "imageCache")
AlertDialog.Builder(context).apply {
setTitle(R.string.warning)
@@ -149,13 +148,8 @@ class SettingsFragment :
}
"backup" -> {
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
- context,
- getDownloadDirectory(context).let {
- if (it.findFile("favorites.json") != null)
- it
- else
- it.createFile("null", "favorites.json")!!
- }
+ File(getDownloadDirectory(context), "favorites.json"),
+ true
)
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
@@ -197,7 +191,7 @@ class SettingsFragment :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
"dl_location" -> {
- findPreference(key)?.summary = getDownloadDirectory(context!!).uri.path
+ findPreference(key)?.summary = getDownloadDirectory(context!!).canonicalPath
}
}
}
@@ -228,7 +222,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"delete_cache" -> {
- val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
+ val dir = File(context.cacheDir, "imageCache")
summary = getDirSize(dir)
onPreferenceClickListener = this@SettingsFragment
@@ -246,7 +240,7 @@ class SettingsFragment :
onPreferenceClickListener = this@SettingsFragment
}
"dl_location" -> {
- summary = getDownloadDirectory(context).uri.path
+ summary = getDownloadDirectory(context).canonicalPath
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
index 807c1379..fecea791 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
@@ -21,4 +21,5 @@ 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
+const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
+const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
\ 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
deleted file mode 100644
index 414da1c8..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
+++ /dev/null
@@ -1,178 +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;
-
-/*
- * 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/download/Cache.kt b/app/src/main/java/xyz/quaver/pupil/util/download/Cache.kt
index 0224c36d..6c75b55d 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.documentfile.provider.DocumentFile
+import android.util.Log
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -34,7 +34,8 @@ import kotlinx.serialization.stringify
import xyz.quaver.Code
import xyz.quaver.hitomi.GalleryBlock
import xyz.quaver.hitomi.Reader
-import xyz.quaver.pupil.util.*
+import xyz.quaver.pupil.util.getCachedGallery
+import xyz.quaver.pupil.util.getDownloadDirectory
import java.io.File
import java.net.URL
@@ -44,29 +45,20 @@ class Cache(context: Context) : ContextWrapper(context) {
// Search in this order
// Download -> Cache
- fun getCachedGallery(galleryID: Int) : DocumentFile? {
- var file = getDownloadDirectory(this).findFile(galleryID.toString())
-
- if (file?.exists() == true)
- return file
-
- file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
-
- return if (file.exists())
- file
- else
- null
+ fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
+ if (!it.exists())
+ it.mkdirs()
}
@UseExperimental(ImplicitReflectionSerializer::class)
fun getCachedMetadata(galleryID: Int) : Metadata? {
- val file = (getCachedGallery(galleryID) ?: return null).findFile(".metadata")
+ val file = File(getCachedGallery(galleryID), ".metadata")
- if (file?.exists() != true)
+ if (!file.exists())
return null
return try {
- Json.parse(file.readText(this))
+ Json.parse(file.readText())
} catch (e: Exception) {
//File corrupted
file.delete()
@@ -76,13 +68,12 @@ class Cache(context: Context) : ContextWrapper(context) {
@UseExperimental(ImplicitReflectionSerializer::class)
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
- val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
- DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
+ val file = File(getCachedGallery(galleryID), ".metadata").also {
if (!it.exists())
- it.mkdirs()
- }).createFile("null", ".metadata") ?: return
+ it.createNewFile()
+ }
- file.writeText(this, Json.stringify(metadata))
+ file.writeText(Json.stringify(metadata))
}
suspend fun getThumbnail(galleryID: Int): String? {
@@ -186,46 +177,39 @@ class Cache(context: Context) : ContextWrapper(context) {
return reader
}
- fun getImages(galleryID: Int): List? {
- val gallery = getCachedGallery(galleryID) ?: return null
+ fun getImages(galleryID: Int): List? {
+ val started = System.currentTimeMillis()
+ val gallery = getCachedGallery(galleryID)
val reader = getReaderOrNull(galleryID) ?: return null
- val images = gallery.listFiles()
+ val images = gallery.listFiles() ?: return null
+ Log.i("PUPILD", "${System.currentTimeMillis() - started} ms")
return reader.galleryInfo.indices.map { index ->
- images.firstOrNull { file -> file.name?.startsWith("%05d".format(index)) == true }
+ images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) }
}
}
fun putImage(galleryID: Int, name: String, data: ByteArray) {
- val cache = getCachedGallery(galleryID) ?:
- DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
+ val cache = File(getCachedGallery(galleryID), name).also {
if (!it.exists())
- it.mkdirs()
- }) ?: return
+ it.createNewFile()
+ }
if (!Regex("""^[0-9]+.+$""").matches(name))
throw IllegalArgumentException("File name is not a number")
- cache.let {
- if (it.findFile(name) != null)
- it
- else
- it.createFile("null", name)
- }?.writeBytes(this, data)
+ cache.writeBytes(data)
}
fun moveToDownload(galleryID: Int) {
- val cache = getCachedGallery(galleryID)
-
- if (cache != null) {
- val download = getDownloadDirectory(this)
+ val cache = getCachedGallery(galleryID).also {
+ if (!it.exists())
+ return
+ }
+ val download = File(getDownloadDirectory(this), galleryID.toString())
- if (!download.isParentOf(cache)) {
- cache.copyRecursively(this, download)
- cache.deleteRecursively()
- }
- } else
- getDownloadDirectory(this).createDirectory(galleryID.toString())
+ cache.copyRecursively(download, true)
+ cache.deleteRecursively()
}
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 647571b3..aa5dda63 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
@@ -214,28 +214,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
- val cache = Cache(this@DownloadWorker).getImages(galleryID)
val lowQuality = preferences.getBoolean("low_quality", false)
- //Cache exists :P
- cache?.get(index)?.let {
- progress[galleryID]?.set(index, Float.POSITIVE_INFINITY)
-
- notify(galleryID)
-
- if (isCompleted(galleryID)) {
- with(Cache(this@DownloadWorker)) {
- if (isDownloading(galleryID)) {
- moveToDownload(galleryID)
- setDownloading(galleryID, false)
- }
- }
- nRunners--
- }
-
- return
- }
-
val request = Request.Builder().apply {
when (reader.code) {
Code.HITOMI -> {
@@ -276,7 +256,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
return@launch
}
- progress.put(galleryID, reader.galleryInfo.map { 0F }.toMutableList())
+ val cache = Cache(this@DownloadWorker).getImages(galleryID)
+
+ progress.put(galleryID, reader.galleryInfo.indices.map { index ->
+ if (cache?.get(index) != null)
+ Float.POSITIVE_INFINITY
+ else
+ 0F
+ }.toMutableList())
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
if (notification[galleryID] == null)
@@ -285,6 +272,18 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notification[galleryID].setContentTitle(reader.title)
notify(galleryID)
+ if (isCompleted(galleryID)) {
+ with(Cache(this@DownloadWorker)) {
+ if (isDownloading(galleryID)) {
+ moveToDownload(galleryID)
+ setDownloading(galleryID, false)
+ }
+ }
+ nRunners--
+
+ return@launch
+ }
+
for (i in reader.galleryInfo.indices) {
val callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
@@ -297,10 +296,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID)
if (isCompleted(galleryID)) {
- val cache = Cache(this@DownloadWorker)
- if (cache.isDownloading(galleryID)) {
- cache.moveToDownload(galleryID)
- cache.setDownloading(galleryID, false)
+ with(Cache(this@DownloadWorker)) {
+ if (isDownloading(galleryID)) {
+ moveToDownload(galleryID)
+ setDownloading(galleryID, false)
+ }
}
nRunners--
}
@@ -319,17 +319,19 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
notify(galleryID)
if (isCompleted(galleryID)) {
- val cache = Cache(this@DownloadWorker)
- if (cache.isDownloading(galleryID)) {
- cache.moveToDownload(galleryID)
- cache.setDownloading(galleryID, false)
+ with(Cache(this@DownloadWorker)) {
+ if (isDownloading(galleryID)) {
+ moveToDownload(galleryID)
+ setDownloading(galleryID, false)
+ }
}
nRunners--
}
}
}
- queueDownload(galleryID, reader, i, callback)
+ if (progress[galleryID]?.get(i)?.isFinite() == true)
+ queueDownload(galleryID, reader, i, callback)
}
}
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 a27629c0..8a25c4b8 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/file.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/file.kt
@@ -18,43 +18,45 @@
package xyz.quaver.pupil.util
+import android.annotation.TargetApi
import android.content.Context
import android.net.Uri
-import androidx.core.content.FileProvider
-import androidx.documentfile.provider.DocumentFile
+import android.os.Build
+import android.os.storage.StorageManager
+import android.provider.DocumentsContract
+import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import java.io.File
+import java.io.FileOutputStream
+import java.lang.reflect.Array
import java.net.URL
-import java.nio.charset.Charset
-import java.util.*
+
fun getCachedGallery(context: Context, galleryID: Int) =
- getDownloadDirectory(context).findFile(galleryID.toString()) ?:
- DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
+ File(getDownloadDirectory(context), galleryID.toString()).let {
+ if (it.exists())
+ it
+ else
+ File(context.cacheDir, "imageCache/$galleryID")
+ }
-fun getDownloadDirectory(context: Context) : DocumentFile {
- val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
- if (it != null)
- Uri.parse(it)
+fun getDownloadDirectory(context: Context) =
+ PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
+ if (it != null && !it.startsWith("content"))
+ File(it)
else
- Uri.fromFile(context.getExternalFilesDir(null))
+ context.getExternalFilesDir(null)!!
}
- return if (uri.toString().startsWith("file"))
- DocumentFile.fromFile(File(uri.path!!))
- else
- DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
-}
+fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
+
+ if (to.parentFile?.exists() == false)
+ to.parentFile!!.mkdirs()
-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
+ if (!to.exists())
+ to.createNewFile()
-fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
- context.contentResolver.openOutputStream(to.uri).use { out ->
- out!!
+ FileOutputStream(to).use { out ->
with(openConnection()) {
val fileSize = contentLength.toLong()
@@ -78,74 +80,135 @@ fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long,
}
}
-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
+fun getExtSdCardPaths(context: Context) =
+ ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
+ it.absolutePath.substringBeforeLast("/Android/data").let { path ->
+ runCatching {
+ File(path).canonicalPath
+ }.getOrElse {
+ path
+ }
+ }
}
- return false
-}
+const val PRIMARY_VOLUME_NAME = "primary"
+fun getVolumePath(context: Context, volumeID: String?): String? {
+ return runCatching {
+ val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
+ val storageVolumeClass = Class.forName("android.os.storage.StorageVolume")
-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.let {
- if (it.findFile(name!!) != null)
- it
- else
- createFile("null", name!!)!!
- }.writeBytes(
- context,
- readBytes(context)
- )
- } else if (this.isDirectory) {
- target.createDirectory(name!!).also { newTarget ->
- listFiles().forEach { child ->
- child.copyRecursively(context, newTarget!!)
+ val getVolumeList = storageVolumeClass.javaClass.getMethod("getVolumeList")
+ val getUUID = storageVolumeClass.getMethod("getUuid")
+ val getPath = storageVolumeClass.getMethod("getPath")
+ val isPrimary = storageVolumeClass.getMethod("isPrimary")
+
+ val result = getVolumeList.invoke(storageManager)!!
+
+ val length = Array.getLength(result)
+
+ for (i in 0 until length) {
+ val storageVolumeElement = Array.get(result, i)
+ val uuid = getUUID.invoke(storageVolumeElement) as? String
+ val primary = isPrimary.invoke(storageVolumeElement) as? Boolean
+
+ // primary volume?
+ if (primary == true && volumeID == PRIMARY_VOLUME_NAME)
+ return@runCatching getPath.invoke(storageVolumeElement) as? String
+
+ // other volumes?
+ if (volumeID == uuid) {
+ return@runCatching getPath.invoke(storageVolumeElement) as? String
}
}
- }
+ return@runCatching null
+ }.getOrNull()
}
-fun DocumentFile.deleteRecursively() {
-
- if (this.isDirectory)
- listFiles().forEach {
- it.deleteRecursively()
- }
+// Credits go to https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri/36162691#36162691
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+fun getVolumeIdFromTreeUri(uri: Uri) =
+ DocumentsContract.getTreeDocumentId(uri).split(':').let {
+ if (it.isNotEmpty())
+ it[0]
+ else
+ null
+ }
- this.delete()
-}
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+fun getDocumentPathFromTreeUri(uri: Uri) =
+ DocumentsContract.getTreeDocumentId(uri).split(':').let {
+ if (it.size >= 2)
+ it[1]
+ else
+ File.separator
+ }
-fun DocumentFile.walk(state: LinkedList = LinkedList()) : Queue {
- if (state.isEmpty())
- state.push(this)
+fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
+ val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
+ it ?: return File.separator
- listFiles().forEach {
- state.push(it)
+ if (it.endsWith(File.separator))
+ it.dropLast(1)
+ else
+ it
+ }
- if (it.isDirectory) {
- it.walk(state)
- }
+ val documentPath = getDocumentPathFromTreeUri(uri).let {
+ if (it.endsWith(File.separator))
+ it.dropLast(1)
+ else
+ it
}
- return state
+ return if (documentPath.isNotEmpty()) {
+ if (documentPath.startsWith(File.separator))
+ volumePath + documentPath
+ else
+ volumePath + File.separator + documentPath
+ } else
+ volumePath
}
-fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
\ No newline at end of file
+// Huge thanks to avluis(https://github.com/avluis)
+// This code is originated from Hentoid(https://github.com/avluis/Hentoid) under Apache-2.0 license.
+fun Uri.toFile(context: Context): File? {
+ val path = this.path ?: return null
+
+ val pathSeparator = path.indexOf(':')
+ val folderName = path.substring(pathSeparator+1)
+
+ // Determine whether the designated file is
+ // - on a removable media (e.g. SD card, OTG)
+ // or
+ // - on the internal phone memory
+ val removableMediaFolderRoots = getExtSdCardPaths(context)
+
+ /* First test is to compare root names with known roots of removable media
+ * In many cases, the SD card root name is shared between pre-SAF (File) and SAF (DocumentFile) frameworks
+ * (e.g. /storage/3437-3934 vs. /tree/3437-3934)
+ * This is what the following block is trying to do
+ */
+ for (s in removableMediaFolderRoots) {
+ val sRoot = s.substring(s.lastIndexOf(File.separatorChar))
+ val root = path.substring(0, pathSeparator).let {
+ it.substring(it.lastIndexOf(File.separatorChar))
+ }
+
+ if (sRoot.equals(root, true)) {
+ return File(s + File.separatorChar + folderName)
+ }
+ }
+ /* In some other cases, there is no common name (e.g. /storage/sdcard1 vs. /tree/3437-3934)
+ * We can use a slower method to translate the Uri obtained with SAF into a pre-SAF path
+ * and compare it to the known removable media volume names
+ */
+ val root = getFullPathFromTreeUri(context, this)
+
+ for (s in removableMediaFolderRoots) {
+ if (root?.startsWith(s) == true) {
+ return File(root)
+ }
+ }
+
+ return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
+}
\ 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 51847f41..1d0c9091 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/update.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt
@@ -26,6 +26,7 @@ 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
@@ -35,6 +36,7 @@ import kotlinx.serialization.json.*
import ru.noties.markwon.Markwon
import xyz.quaver.pupil.BuildConfig
import xyz.quaver.pupil.R
+import java.io.File
import java.net.URL
import java.util.*
@@ -146,15 +148,10 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
}
CoroutineScope(Dispatchers.IO).launch io@{
- val target = getDownloadDirectory(context).let {
- if (it.findFile("Pupil.apk") != null)
- it
- else
- it.createFile("null", "Pupil.apk")!!
- }
+ val target = File(getDownloadDirectory(context), "Pupil.apk")
try {
- URL(url).download(context, target) { progress, fileSize ->
+ URL(url).download(target) { progress, fileSize ->
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
}
@@ -173,7 +170,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(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
+ setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
}
builder.apply {