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