Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19308d840a | ||
|
|
46bd1318cd | ||
|
|
9d1998fe52 | ||
|
|
a714a8230b | ||
|
|
b5432cd0b4 | ||
|
|
5634e94f3e | ||
|
|
c1a71b0db3 | ||
|
|
d93e7f8834 | ||
|
|
3175b2c45c | ||
|
|
547b6e8e3b | ||
|
|
d88ac27e72 | ||
|
|
e551a40d08 |
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId "xyz.quaver.pupil"
|
applicationId "xyz.quaver.pupil"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 36
|
versionCode 41
|
||||||
versionName "5.3-beta3"
|
versionName "4.5"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -41,9 +41,6 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
|
||||||
}
|
|
||||||
buildToolsVersion = '29.0.2'
|
buildToolsVersion = '29.0.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +69,14 @@ dependencies {
|
|||||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||||
implementation 'com.github.clans:fab:1.6.4'
|
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'
|
||||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.11.0') {
|
implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") {
|
||||||
transitive = false
|
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.github.chrisbanes:PhotoView:2.3.0'
|
||||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.10.0'
|
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":36,"versionName":"5.3-beta3","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"4.5","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||||
@@ -26,11 +26,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import kotlinx.coroutines.runBlocking
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import xyz.quaver.hiyobi.cookie
|
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.ui.LockActivity
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
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 java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
@@ -58,8 +50,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
Log.i("PUPILD", getDownloadDirectory(appContext).absolutePath ?: "")
|
|
||||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -88,40 +78,6 @@ class ExampleInstrumentedTest {
|
|||||||
Log.d("Pupil", data.size.toString())
|
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
|
@Test
|
||||||
fun test_downloadWorker() {
|
fun test_downloadWorker() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
android:maxSdkVersion="21" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import android.app.Notification
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -46,19 +45,19 @@ class Pupil : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
try {
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
PreferenceManager.getDefaultSharedPreferences(this).getInt("dl_location", 0)
|
||||||
|
|
||||||
val download = try {
|
|
||||||
preference.getString("dl_location", null)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
preference.edit().remove("dl_location").apply()
|
preference.edit().remove("dl_location").apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download == null) {
|
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||||
val default = ContextCompat.getExternalFilesDirs(this, null)[0]
|
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||||
preference.edit().putString("dl_location", Uri.fromFile(default).toString()).apply()
|
|
||||||
}
|
val file = preference.getString("dl_location", null)
|
||||||
|
|
||||||
|
if (file?.startsWith("content") == true)
|
||||||
|
preference.edit().remove("dl_location").apply()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
|
|||||||
@@ -71,44 +71,46 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
inner class GalleryViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
var timerTask: TimerTask? = null
|
var timerTask: TimerTask? = null
|
||||||
|
|
||||||
fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.Main).launch {
|
private fun updateProgress(context: Context, galleryID: Int) = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val cache = Cache(context).getCachedGallery(galleryID)
|
val cache = Cache(context).getCachedGallery(galleryID)
|
||||||
val reader = Cache(context).getReaderOrNull(galleryID)
|
val reader = Cache(context).getReaderOrNull(galleryID)
|
||||||
|
|
||||||
if (reader == null) {
|
launch(Dispatchers.Main) main@{
|
||||||
view.galleryblock_progressbar.visibility = View.GONE
|
if (reader == null) {
|
||||||
view.galleryblock_progress_complete.visibility = View.GONE
|
view.galleryblock_progressbar.visibility = View.GONE
|
||||||
return@launch
|
view.galleryblock_progress_complete.visibility = View.GONE
|
||||||
}
|
return@main
|
||||||
|
|
||||||
with(view.galleryblock_progressbar) {
|
|
||||||
|
|
||||||
progress = cache?.listFiles()?.count { file ->
|
|
||||||
Regex("^[0-9]+.+\$").matches(file.name!!)
|
|
||||||
} ?: 0
|
|
||||||
|
|
||||||
if (visibility == View.GONE) {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
max = reader.galleryInfo.size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress == max) {
|
with(view.galleryblock_progressbar) {
|
||||||
if (completeFlag.get(galleryID, false)) {
|
|
||||||
with(view.galleryblock_progress_complete) {
|
progress = cache.listFiles()?.count { file ->
|
||||||
setImageResource(R.drawable.ic_progressbar)
|
Regex("^[0-9]+.+\$").matches(file.name)
|
||||||
visibility = View.VISIBLE
|
} ?: 0
|
||||||
}
|
|
||||||
} else {
|
if (visibility == View.GONE) {
|
||||||
with(view.galleryblock_progress_complete) {
|
visibility = View.VISIBLE
|
||||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
max = reader.galleryInfo.size
|
||||||
this?.start()
|
|
||||||
})
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
completeFlag.put(galleryID, true)
|
|
||||||
}
|
}
|
||||||
} else
|
|
||||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
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)
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +154,10 @@ class GalleryBlockAdapter(context: Context, private val galleries: List<GalleryB
|
|||||||
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
val cache = Cache(context).getCachedGallery(galleryBlock.id)
|
||||||
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
val reader = Cache(context).getReaderOrNull(galleryBlock.id)
|
||||||
|
|
||||||
if (cache != null && reader != null) {
|
if (reader != null) {
|
||||||
val count = cache.listFiles().count {
|
val count = cache.listFiles()?.count {
|
||||||
Regex("^[0-9]+.+\$").matches(it.name!!)
|
Regex("^[0-9]+.+\$").matches(it.name)
|
||||||
}
|
} ?: 0
|
||||||
|
|
||||||
with(galleryblock_progressbar) {
|
with(galleryblock_progressbar) {
|
||||||
max = reader.galleryInfo.size
|
max = reader.galleryInfo.size
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import android.view.ViewGroup
|
|||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.ListPreloader
|
||||||
|
import com.bumptech.glide.RequestBuilder
|
||||||
|
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.crashlytics.android.Crashlytics
|
import com.crashlytics.android.Crashlytics
|
||||||
import io.fabric.sdk.android.Fabric
|
import io.fabric.sdk.android.Fabric
|
||||||
@@ -37,6 +40,7 @@ import xyz.quaver.pupil.BuildConfig
|
|||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
import xyz.quaver.pupil.util.download.Cache
|
import xyz.quaver.pupil.util.download.Cache
|
||||||
import xyz.quaver.pupil.util.download.DownloadWorker
|
import xyz.quaver.pupil.util.download.DownloadWorker
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -44,12 +48,48 @@ import kotlin.math.roundToInt
|
|||||||
class ReaderAdapter(private val context: Context,
|
class ReaderAdapter(private val context: Context,
|
||||||
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
private val galleryID: Int) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||||
|
|
||||||
var isFullScreen = false
|
//region Glide.RecyclerView
|
||||||
|
inner class SizeProvider : ListPreloader.PreloadSizeProvider<File> {
|
||||||
|
|
||||||
|
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<File> {
|
||||||
|
|
||||||
|
override fun getPreloadItems(position: Int): MutableList<File> {
|
||||||
|
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
|
var reader: Reader? = null
|
||||||
private val glide = Glide.with(context)
|
val glide = Glide.with(context)
|
||||||
val timer = Timer()
|
val timer = Timer()
|
||||||
|
|
||||||
|
val sizeProvider = SizeProvider()
|
||||||
|
val modelProvider = ModelProvider()
|
||||||
|
val preloader = RecyclerViewPreloader<File>(glide, modelProvider, sizeProvider, 10)
|
||||||
|
|
||||||
|
var isFullScreen = false
|
||||||
|
|
||||||
var onItemClickListener : ((Int) -> (Unit))? = null
|
var onItemClickListener : ((Int) -> (Unit))? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -92,46 +132,47 @@ class ReaderAdapter(private val context: Context,
|
|||||||
|
|
||||||
holder.view.reader_index.text = (position+1).toString()
|
holder.view.reader_index.text = (position+1).toString()
|
||||||
|
|
||||||
val images = Cache(context).getImages(galleryID)
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val images = Cache(context).getImages(galleryID)
|
||||||
|
|
||||||
if (images?.get(position) != null) {
|
launch(Dispatchers.Main) {
|
||||||
glide
|
if (images?.get(position) != null) {
|
||||||
.load(images[position]?.uri)
|
glide
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.load(images[position])
|
||||||
.skipMemoryCache(true)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.error(R.drawable.image_broken_variant)
|
.skipMemoryCache(true)
|
||||||
.apply {
|
.error(R.drawable.image_broken_variant)
|
||||||
if (BuildConfig.CENSOR)
|
.apply {
|
||||||
override(5, 8)
|
if (BuildConfig.CENSOR)
|
||||||
}
|
override(5, 8)
|
||||||
.into(holder.view.image)
|
}
|
||||||
} else {
|
.into(holder.view.image)
|
||||||
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
} else {
|
||||||
|
val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position)
|
||||||
|
|
||||||
if (progress?.isNaN() == true) {
|
if (progress?.isNaN() == true) {
|
||||||
|
|
||||||
if (Fabric.isInitialized())
|
if (Fabric.isInitialized())
|
||||||
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position))
|
||||||
|
|
||||||
glide
|
glide
|
||||||
.load(R.drawable.image_broken_variant)
|
.load(R.drawable.image_broken_variant)
|
||||||
.into(holder.view.image)
|
.into(holder.view.image)
|
||||||
|
} else {
|
||||||
|
holder.view.reader_item_progressbar.progress =
|
||||||
|
if (progress?.isInfinite() == true)
|
||||||
|
100
|
||||||
|
else
|
||||||
|
progress?.roundToInt() ?: 0
|
||||||
|
|
||||||
return
|
holder.view.image.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.view.reader_item_progressbar.progress =
|
timer.schedule(1000) {
|
||||||
if (progress?.isInfinite() == true)
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
100
|
notifyItemChanged(position)
|
||||||
else
|
}
|
||||||
progress?.roundToInt() ?: 0
|
}
|
||||||
|
|
||||||
holder.view.image.setImageDrawable(null)
|
|
||||||
|
|
||||||
|
|
||||||
timer.schedule(1000) {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,12 +429,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
DownloadWorker.getInstance(context).cancel(galleryID)
|
DownloadWorker.getInstance(context).cancel(galleryID)
|
||||||
|
|
||||||
var cache = Cache(context).getCachedGallery(galleryID)
|
Cache(context).getCachedGallery(galleryID).deleteRecursively()
|
||||||
|
|
||||||
while (cache != null) {
|
|
||||||
cache.deleteRecursively()
|
|
||||||
cache = Cache(context).getCachedGallery(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
histories.remove(galleryID)
|
histories.remove(galleryID)
|
||||||
|
|
||||||
@@ -969,11 +964,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode.DOWNLOAD -> {
|
Mode.DOWNLOAD -> {
|
||||||
val downloads = getDownloadDirectory(this@MainActivity)?.listFiles()?.filter { file ->
|
val downloads = getDownloadDirectory(this@MainActivity).listFiles()?.filter { file ->
|
||||||
file.isDirectory && (file.name!!.toIntOrNull() != null) && file.findFile(".metadata") != null
|
file.isDirectory && (file.name.toIntOrNull() != null) && File(file, ".metadata").exists()
|
||||||
}?.map {
|
}?.map {
|
||||||
it.name!!.toInt()
|
it.name.toInt()
|
||||||
}?: listOf()
|
} ?: emptyList()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
query.isEmpty() -> downloads.apply {
|
query.isEmpty() -> downloads.apply {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
queue.add(galleryID)
|
queue.add(galleryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer.schedule(0, 1000) {
|
timer.schedule(1000, 1000) {
|
||||||
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
if (worker.progress.indexOfKey(galleryID) < 0) //loading
|
||||||
return@schedule
|
return@schedule
|
||||||
|
|
||||||
@@ -296,6 +296,7 @@ class ReaderActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//addOnScrollListener((adapter as ReaderAdapter).preloader)
|
||||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
|||||||
@@ -18,15 +18,15 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
package xyz.quaver.pupil.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
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.R
|
||||||
import xyz.quaver.pupil.ui.fragment.LockFragment
|
import xyz.quaver.pupil.ui.fragment.LockFragment
|
||||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
import xyz.quaver.pupil.util.*
|
||||||
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.io.File
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
@@ -124,16 +121,23 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
REQUEST_DOWNLOAD_FOLDER -> {
|
REQUEST_DOWNLOAD_FOLDER -> {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
data?.data?.also { uri ->
|
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)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||||
|
|
||||||
if (DocumentFile.fromTreeUri(this, uri)?.canWrite() == false)
|
val file = uri.toFile(this)
|
||||||
Snackbar.make(settings, R.string.settings_dl_location_not_writable, Snackbar.LENGTH_LONG).show()
|
|
||||||
|
if (file?.canWrite() != true)
|
||||||
|
Snackbar.make(
|
||||||
|
settings,
|
||||||
|
R.string.settings_dl_location_not_writable,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).show()
|
||||||
else
|
else
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString("dl_location", uri.toString())
|
.putString("dl_location", file.canonicalPath)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,14 +147,33 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
val directory = data?.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)!!
|
||||||
|
|
||||||
if (!File(directory).canWrite())
|
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
|
else
|
||||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||||
.putString("dl_location", Uri.fromFile(File(directory)).toString())
|
.putString("dl_location", File(directory).canonicalPath)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,16 +18,18 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.ui.dialog
|
package xyz.quaver.pupil.ui.dialog
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.android.synthetic.main.item_dl_location.view.*
|
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.R
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER
|
||||||
import xyz.quaver.pupil.util.REQUEST_DOWNLOAD_FOLDER_OLD
|
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 xyz.quaver.pupil.util.byteToString
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
||||||
|
|
||||||
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
private val preference = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
private val buttons = mutableListOf<Pair<RadioButton, Uri?>>()
|
private val buttons = mutableListOf<Pair<RadioButton, File?>>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout
|
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
|
pair.first.isChecked = false
|
||||||
}
|
}
|
||||||
button.performClick()
|
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()
|
button.performClick()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
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()
|
dismiss()
|
||||||
} else { // Can't use SAF on old Androids!
|
} else { // Can't use SAF on old Androids!
|
||||||
@@ -106,9 +115,9 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) {
|
|||||||
buttons.add(button to null)
|
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 {
|
val index = externalFilesDirs.indexOfFirst {
|
||||||
Uri.fromFile(it).toString() == pref.toString()
|
it.canonicalPath == pref
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
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()
|
val size = dir.walk().map { it.length() }.sum()
|
||||||
|
|
||||||
return getString(R.string.settings_clear_summary, byteToString(size))
|
return getString(R.string.settings_clear_summary, byteToString(size))
|
||||||
@@ -86,7 +85,7 @@ class SettingsFragment :
|
|||||||
checkUpdate(activity as SettingsActivity, true)
|
checkUpdate(activity as SettingsActivity, true)
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
val dir = File(context.cacheDir, "imageCache")
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
setTitle(R.string.warning)
|
setTitle(R.string.warning)
|
||||||
@@ -149,8 +148,8 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
"backup" -> {
|
"backup" -> {
|
||||||
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
File(ContextCompat.getDataDir(context), "favorites.json").copyTo(
|
||||||
context,
|
File(getDownloadDirectory(context), "favorites.json"),
|
||||||
getDownloadDirectory(context).createFile("null", "favorites.json")!!
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
Snackbar.make(this@SettingsFragment.listView, R.string.settings_backup_snackbar, Snackbar.LENGTH_LONG)
|
||||||
@@ -192,7 +191,7 @@ class SettingsFragment :
|
|||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
when (key) {
|
when (key) {
|
||||||
"dl_location" -> {
|
"dl_location" -> {
|
||||||
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).uri.path
|
findPreference<Preference>(key)?.summary = getDownloadDirectory(context!!).canonicalPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +222,7 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"delete_cache" -> {
|
"delete_cache" -> {
|
||||||
val dir = DocumentFile.fromFile(File(context.cacheDir, "imageCache"))
|
val dir = File(context.cacheDir, "imageCache")
|
||||||
summary = getDirSize(dir)
|
summary = getDirSize(dir)
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
@@ -241,7 +240,7 @@ class SettingsFragment :
|
|||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
"dl_location" -> {
|
"dl_location" -> {
|
||||||
summary = getDownloadDirectory(context).uri.path
|
summary = getDownloadDirectory(context).canonicalPath
|
||||||
|
|
||||||
onPreferenceClickListener = this@SettingsFragment
|
onPreferenceClickListener = this@SettingsFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ package xyz.quaver.pupil.util
|
|||||||
const val REQUEST_LOCK = 38238
|
const val REQUEST_LOCK = 38238
|
||||||
const val REQUEST_RESTORE = 16546
|
const val REQUEST_RESTORE = 16546
|
||||||
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
const val REQUEST_DOWNLOAD_FOLDER = 3874
|
||||||
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425
|
||||||
|
const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900
|
||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ package xyz.quaver.pupil.util.download
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import android.util.Log
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -34,7 +34,8 @@ import kotlinx.serialization.stringify
|
|||||||
import xyz.quaver.Code
|
import xyz.quaver.Code
|
||||||
import xyz.quaver.hitomi.GalleryBlock
|
import xyz.quaver.hitomi.GalleryBlock
|
||||||
import xyz.quaver.hitomi.Reader
|
import xyz.quaver.hitomi.Reader
|
||||||
import xyz.quaver.pupil.util.*
|
import xyz.quaver.pupil.util.getCachedGallery
|
||||||
|
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
@@ -44,29 +45,20 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
|
|
||||||
// Search in this order
|
// Search in this order
|
||||||
// Download -> Cache
|
// Download -> Cache
|
||||||
fun getCachedGallery(galleryID: Int) : DocumentFile? {
|
fun getCachedGallery(galleryID: Int) = getCachedGallery(this, galleryID).also {
|
||||||
var file = getDownloadDirectory(this).findFile(galleryID.toString())
|
if (!it.exists())
|
||||||
|
it.mkdirs()
|
||||||
if (file?.exists() == true)
|
|
||||||
return file
|
|
||||||
|
|
||||||
file = DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID"))
|
|
||||||
|
|
||||||
return if (file.exists())
|
|
||||||
file
|
|
||||||
else
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||||
fun getCachedMetadata(galleryID: Int) : Metadata? {
|
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 null
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
Json.parse(file.readText(this))
|
Json.parse(file.readText())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//File corrupted
|
//File corrupted
|
||||||
file.delete()
|
file.delete()
|
||||||
@@ -76,13 +68,12 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
|
|
||||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||||
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
fun setCachedMetadata(galleryID: Int, metadata: Metadata) {
|
||||||
val file = getCachedGallery(galleryID)?.findFile(".metadata") ?:
|
val file = File(getCachedGallery(galleryID), ".metadata").also {
|
||||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.mkdirs()
|
it.createNewFile()
|
||||||
}).createFile("null", ".metadata") ?: return
|
}
|
||||||
|
|
||||||
file.writeText(this, Json.stringify(metadata))
|
file.writeText(Json.stringify(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getThumbnail(galleryID: Int): String? {
|
suspend fun getThumbnail(galleryID: Int): String? {
|
||||||
@@ -186,41 +177,39 @@ class Cache(context: Context) : ContextWrapper(context) {
|
|||||||
return reader
|
return reader
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
fun getImages(galleryID: Int): List<File?>? {
|
||||||
val gallery = getCachedGallery(galleryID) ?: return null
|
val started = System.currentTimeMillis()
|
||||||
|
val gallery = getCachedGallery(galleryID)
|
||||||
val reader = getReaderOrNull(galleryID) ?: return null
|
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 ->
|
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) {
|
fun putImage(galleryID: Int, name: String, data: ByteArray) {
|
||||||
val cache = getCachedGallery(galleryID) ?:
|
val cache = File(getCachedGallery(galleryID), name).also {
|
||||||
DocumentFile.fromFile(File(cacheDir, "imageCache/$galleryID").also {
|
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.mkdirs()
|
it.createNewFile()
|
||||||
}) ?: return
|
}
|
||||||
|
|
||||||
if (!Regex("""^[0-9]+.+$""").matches(name))
|
if (!Regex("""^[0-9]+.+$""").matches(name))
|
||||||
throw IllegalArgumentException("File name is not a number")
|
throw IllegalArgumentException("File name is not a number")
|
||||||
|
|
||||||
cache.createFile("null", name)?.writeBytes(this, data)
|
cache.writeBytes(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToDownload(galleryID: Int) {
|
fun moveToDownload(galleryID: Int) {
|
||||||
val cache = getCachedGallery(galleryID)
|
val cache = getCachedGallery(galleryID).also {
|
||||||
|
if (!it.exists())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val download = File(getDownloadDirectory(this), galleryID.toString())
|
||||||
|
|
||||||
if (cache != null) {
|
cache.copyRecursively(download, true)
|
||||||
val download = getDownloadDirectory(this)
|
cache.deleteRecursively()
|
||||||
|
|
||||||
if (!download.isParentOf(cache)) {
|
|
||||||
cache.copyRecursively(this, download)
|
|
||||||
cache.deleteRecursively()
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
getDownloadDirectory(this).createDirectory(galleryID.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
fun isDownloading(galleryID: Int) = getCachedMetadata(galleryID)?.isDownloading == true
|
||||||
|
|||||||
@@ -214,28 +214,8 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
fun isCompleted(galleryID: Int) = progress[galleryID]?.all { !it.isFinite() } == true
|
||||||
|
|
||||||
private fun queueDownload(galleryID: Int, reader: Reader, index: Int, callback: Callback) {
|
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)
|
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 {
|
val request = Request.Builder().apply {
|
||||||
when (reader.code) {
|
when (reader.code) {
|
||||||
Code.HITOMI -> {
|
Code.HITOMI -> {
|
||||||
@@ -276,7 +256,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
return@launch
|
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())
|
exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList())
|
||||||
|
|
||||||
if (notification[galleryID] == null)
|
if (notification[galleryID] == null)
|
||||||
@@ -285,6 +272,18 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
notification[galleryID].setContentTitle(reader.title)
|
notification[galleryID].setContentTitle(reader.title)
|
||||||
notify(galleryID)
|
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) {
|
for (i in reader.galleryInfo.indices) {
|
||||||
val callback = object : Callback {
|
val callback = object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
@@ -297,10 +296,11 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
if (isCompleted(galleryID)) {
|
||||||
val cache = Cache(this@DownloadWorker)
|
with(Cache(this@DownloadWorker)) {
|
||||||
if (cache.isDownloading(galleryID)) {
|
if (isDownloading(galleryID)) {
|
||||||
cache.moveToDownload(galleryID)
|
moveToDownload(galleryID)
|
||||||
cache.setDownloading(galleryID, false)
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
nRunners--
|
||||||
}
|
}
|
||||||
@@ -319,17 +319,19 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont
|
|||||||
notify(galleryID)
|
notify(galleryID)
|
||||||
|
|
||||||
if (isCompleted(galleryID)) {
|
if (isCompleted(galleryID)) {
|
||||||
val cache = Cache(this@DownloadWorker)
|
with(Cache(this@DownloadWorker)) {
|
||||||
if (cache.isDownloading(galleryID)) {
|
if (isDownloading(galleryID)) {
|
||||||
cache.moveToDownload(galleryID)
|
moveToDownload(galleryID)
|
||||||
cache.setDownloading(galleryID, false)
|
setDownloading(galleryID, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nRunners--
|
nRunners--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queueDownload(galleryID, reader, i, callback)
|
if (progress[galleryID]?.get(i)?.isFinite() == true)
|
||||||
|
queueDownload(galleryID, reader, i, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,43 +18,45 @@
|
|||||||
|
|
||||||
package xyz.quaver.pupil.util
|
package xyz.quaver.pupil.util
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.content.FileProvider
|
import android.os.Build
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import android.os.storage.StorageManager
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.lang.reflect.Array
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun getCachedGallery(context: Context, galleryID: Int) =
|
fun getCachedGallery(context: Context, galleryID: Int) =
|
||||||
getDownloadDirectory(context).findFile(galleryID.toString()) ?:
|
File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||||
DocumentFile.fromFile(File(context.cacheDir, "imageCache/$galleryID"))
|
if (it.exists())
|
||||||
|
it
|
||||||
fun getDownloadDirectory(context: Context) : DocumentFile {
|
|
||||||
val uri = PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
|
||||||
if (it != null)
|
|
||||||
Uri.parse(it)
|
|
||||||
else
|
else
|
||||||
Uri.fromFile(context.getExternalFilesDir(null))
|
File(context.cacheDir, "imageCache/$galleryID")
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (uri.toString().startsWith("file"))
|
fun getDownloadDirectory(context: Context) =
|
||||||
DocumentFile.fromFile(File(uri.path!!))
|
PreferenceManager.getDefaultSharedPreferences(context).getString("dl_location", null).let {
|
||||||
else
|
if (it != null && !it.startsWith("content"))
|
||||||
DocumentFile.fromTreeUri(context, uri) ?: DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
|
File(it)
|
||||||
}
|
else
|
||||||
|
context.getExternalFilesDir(null)!!
|
||||||
|
}
|
||||||
|
|
||||||
fun convertUpdateUri(context: Context, uri: Uri) : Uri =
|
fun URL.download(to: File, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
||||||
if (uri.toString().startsWith("file"))
|
|
||||||
FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(uri.path!!.substringAfter("file:///")))
|
|
||||||
else
|
|
||||||
uri
|
|
||||||
|
|
||||||
fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long, Long) -> Unit)? = null) {
|
if (to.parentFile?.exists() == false)
|
||||||
context.contentResolver.openOutputStream(to.uri).use { out ->
|
to.parentFile!!.mkdirs()
|
||||||
out!!
|
|
||||||
|
if (!to.exists())
|
||||||
|
to.createNewFile()
|
||||||
|
|
||||||
|
FileOutputStream(to).use { out ->
|
||||||
|
|
||||||
with(openConnection()) {
|
with(openConnection()) {
|
||||||
val fileSize = contentLength.toLong()
|
val fileSize = contentLength.toLong()
|
||||||
@@ -78,69 +80,135 @@ fun URL.download(context: Context, to: DocumentFile, onDownloadProgress: ((Long,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DocumentFile.isParentOf(file: DocumentFile?) : Boolean {
|
fun getExtSdCardPaths(context: Context) =
|
||||||
var parent = file?.parentFile
|
ContextCompat.getExternalFilesDirs(context, null).drop(1).map {
|
||||||
while (parent != null) {
|
it.absolutePath.substringBeforeLast("/Android/data").let { path ->
|
||||||
if (this.uri.path == parent.uri.path)
|
runCatching {
|
||||||
return true
|
File(path).canonicalPath
|
||||||
|
}.getOrElse {
|
||||||
parent = parent.parentFile
|
path
|
||||||
}
|
|
||||||
|
|
||||||
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!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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() {
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isDirectory)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
listFiles().forEach {
|
fun getDocumentPathFromTreeUri(uri: Uri) =
|
||||||
it.deleteRecursively()
|
DocumentsContract.getTreeDocumentId(uri).split(':').let {
|
||||||
|
if (it.size >= 2)
|
||||||
|
it[1]
|
||||||
|
else
|
||||||
|
File.separator
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFullPathFromTreeUri(context: Context, uri: Uri) : String? {
|
||||||
|
val volumePath = getVolumePath(context, getVolumeIdFromTreeUri(uri) ?: return null).let {
|
||||||
|
it ?: return File.separator
|
||||||
|
|
||||||
|
if (it.endsWith(File.separator))
|
||||||
|
it.dropLast(1)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
val documentPath = getDocumentPathFromTreeUri(uri).let {
|
||||||
|
if (it.endsWith(File.separator))
|
||||||
|
it.dropLast(1)
|
||||||
|
else
|
||||||
|
it
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (documentPath.isNotEmpty()) {
|
||||||
|
if (documentPath.startsWith(File.separator))
|
||||||
|
volumePath + documentPath
|
||||||
|
else
|
||||||
|
volumePath + File.separator + documentPath
|
||||||
|
} else
|
||||||
|
volumePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delete()
|
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)
|
||||||
|
|
||||||
fun DocumentFile.walk(state: LinkedList<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
|
for (s in removableMediaFolderRoots) {
|
||||||
if (state.isEmpty())
|
if (root?.startsWith(s) == true) {
|
||||||
state.push(this)
|
return File(root)
|
||||||
|
|
||||||
listFiles().forEach {
|
|
||||||
state.push(it)
|
|
||||||
|
|
||||||
if (it.isDirectory) {
|
|
||||||
it.walk(state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return File(context.getExternalFilesDir(null)?.canonicalPath?.substringBeforeLast("/Android/data") ?: return null, folderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun File.copyTo(context: Context, target: DocumentFile) = target.writeBytes(context, this.readBytes())
|
|
||||||
@@ -26,6 +26,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -35,6 +36,7 @@ import kotlinx.serialization.json.*
|
|||||||
import ru.noties.markwon.Markwon
|
import ru.noties.markwon.Markwon
|
||||||
import xyz.quaver.pupil.BuildConfig
|
import xyz.quaver.pupil.BuildConfig
|
||||||
import xyz.quaver.pupil.R
|
import xyz.quaver.pupil.R
|
||||||
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -146,10 +148,10 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch io@{
|
CoroutineScope(Dispatchers.IO).launch io@{
|
||||||
val target = getDownloadDirectory(context)?.createFile("null", "Pupil.apk")!!
|
val target = File(getDownloadDirectory(context), "Pupil.apk")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
URL(url).download(context, target) { progress, fileSize ->
|
URL(url).download(target) { progress, fileSize ->
|
||||||
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
builder.setProgress(fileSize.toInt(), progress.toInt(), false)
|
||||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
@@ -168,7 +170,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) {
|
|||||||
|
|
||||||
val install = Intent(Intent.ACTION_VIEW).apply {
|
val install = Intent(Intent.ACTION_VIEW).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
setDataAndType(convertUpdateUri(context, target.uri), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
setDataAndType(FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", target), MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.apply {
|
builder.apply {
|
||||||
|
|||||||
Reference in New Issue
Block a user