Added Custom download folder
This commit is contained in:
24
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
24
app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
178
app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
Normal file
178
app/src/main/java/xyz/quaver/pupil/util/FileUtils.java
Normal file
@@ -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 <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());
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Reader?>?
|
||||
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<String>) -> Unit)? = null
|
||||
var onErrorHandler: ((Exception) -> Unit)? = null
|
||||
var onCompleteHandler: (() -> Unit)? = null
|
||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
||||
|
||||
companion object : SparseArray<GalleryDownloader>()
|
||||
|
||||
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<String>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<File?>? {
|
||||
fun getImages(galleryID: Int): List<DocumentFile?>? {
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
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<DocumentFile> = LinkedList()) : Queue<DocumentFile> {
|
||||
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())
|
||||
@@ -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<File> {
|
||||
val oldGallery = mutableListOf<File>()
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user