/* * Pupil, Hitomi.la viewer for Android * Copyright (C) 2019 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.annotation.SuppressLint import android.app.DownloadManager import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.util.Base64 import android.webkit.URLUtil import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.preference.PreferenceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* import okhttp3.Call import okhttp3.Callback import okhttp3.Request import okhttp3.Response import ru.noties.markwon.Markwon import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.Reader import xyz.quaver.hitomi.getGalleryBlock import xyz.quaver.hitomi.getReader import xyz.quaver.io.FileX import xyz.quaver.io.util.getChild import xyz.quaver.io.util.readText import xyz.quaver.io.util.writeBytes import xyz.quaver.io.util.writeText import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.R import xyz.quaver.pupil.client import xyz.quaver.pupil.favorites import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.Metadata import java.io.File import java.io.IOException import java.net.URL import java.util.* fun getReleases(url: String) : JsonArray { return try { URL(url).readText().let { Json.parseToJsonElement(it).jsonArray } } catch (e: Exception) { JsonArray(emptyList()) } } fun checkUpdate(url: String) : JsonObject? { val releases = getReleases(url) if (releases.isEmpty()) return null return releases.firstOrNull { Preferences["beta"] || it.jsonObject["prerelease"]?.jsonPrimitive?.booleanOrNull == false }?.let { if (it.jsonObject["tag_name"]?.jsonPrimitive?.contentOrNull == BuildConfig.VERSION_NAME) null else it.jsonObject } } fun getApkUrl(releases: JsonObject) : String? { return releases["assets"]?.jsonArray?.firstOrNull { Regex("Pupil-v.+\\.apk").matches(it.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: "") }.let { it?.jsonObject?.get("browser_download_url")?.jsonPrimitive?.contentOrNull } } fun checkUpdate(context: Context, force: Boolean = false) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) val ignoreUpdateUntil = preferences.getLong("ignore_update_until", 0) if (!force && ignoreUpdateUntil > System.currentTimeMillis()) return fun extractReleaseNote(update: JsonObject, locale: Locale) : String { val markdown = update["body"]!!.jsonPrimitive.content val target = when(locale.language) { "ko" -> "한국어" "ja" -> "日本語" else -> "English" } val releaseNote = Regex("^# Release Note.+$") val language = Regex("^## $target$") val end = Regex("^#.+$") var releaseNoteFlag = false var languageFlag = false val result = StringBuilder() for(line in markdown.lines()) { if (releaseNote.matches(line)) { releaseNoteFlag = true continue } if (releaseNoteFlag) { if (language.matches(line)) { languageFlag = true continue } } if (languageFlag) { if (end.matches(line)) break result.append(line+"\n") } } return context.getString(R.string.update_release_note, update["tag_name"]?.jsonPrimitive?.contentOrNull, result.toString()) } CoroutineScope(Dispatchers.Default).launch { val update = checkUpdate(context.getString(R.string.release_url)) ?: return@launch val url = getApkUrl(update) ?: return@launch val dialog = AlertDialog.Builder(context).apply { setTitle(R.string.update_title) val msg = extractReleaseNote(update, Locale.getDefault()) setMessage(Markwon.create(context).toMarkdown(msg)) setPositiveButton(android.R.string.ok) { _, _ -> val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager //Cancel any download queued before val id: Long = Preferences["update_download_id"] if (id != -1L) downloadManager.remove(id) val target = File(context.getExternalFilesDir(null), "Pupil.apk").also { it.delete() } val request = DownloadManager.Request(Uri.parse(url)) .setTitle(context.getText(R.string.update_notification_description)) .setDestinationUri(Uri.fromFile(target)) downloadManager.enqueue(request).also { Preferences["update_download_id"] = it } } setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ -> if (!force) preferences.edit() .putLong("ignore_update_until", System.currentTimeMillis() + 604800000) .apply() } } launch(Dispatchers.Main) { dialog.show() } } } fun restore(url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((List) -> Unit)? = null) { if (!URLUtil.isValidUrl(url)) { onFailure?.invoke(IllegalArgumentException()) return } val request = Request.Builder() .url(url) .get() .build() client.newCall(request).enqueue(object: Callback { override fun onFailure(call: Call, e: IOException) { onFailure?.invoke(e) } override fun onResponse(call: Call, response: Response) { kotlin.runCatching { Json.decodeFromString>(response.also { if (it.code() != 200) throw IOException() }.body().use { it?.string() } ?: "[]").let { favorites.addAll(it) onSuccess?.invoke(it) } }.onFailure { onFailure?.invoke(it) } } }) } private var job: Job? = null private val receiver = object: BroadcastReceiver() { val ACTION_CANCEL = "ACTION_IMPORT_CANCEL" override fun onReceive(context: Context?, intent: Intent?) { context ?: return when (intent?.action) { ACTION_CANCEL -> { job?.cancel() NotificationManagerCompat.from(context).cancel(R.id.notification_id_import) context.unregisterReceiver(this) } } } } @SuppressLint("RestrictedApi") fun xyz.quaver.pupil.util.downloader.DownloadManager.migrate() { registerReceiver(receiver, IntentFilter().apply { addAction(receiver.ACTION_CANCEL) }) val notificationManager = NotificationManagerCompat.from(this) val action = NotificationCompat.Action.Builder(0, getText(android.R.string.cancel), PendingIntent.getBroadcast(this, R.id.notification_import_cancel_action.normalizeID(), Intent(receiver.ACTION_CANCEL), PendingIntent.FLAG_UPDATE_CURRENT) ).build() val notification = NotificationCompat.Builder(this, "import") .setContentTitle(getText(R.string.import_old_galleries_notification)) .setProgress(0, 0, true) .addAction(action) .setSmallIcon(R.drawable.ic_notification) .setOngoing(true) DownloadService.cancel(this) job?.cancel() job = CoroutineScope(Dispatchers.IO).launch { val images = listOf( "jpg", "png", "gif", "webp" ) val downloadFolders = downloadFolder.listFiles { folder -> folder.isDirectory && !downloadFolderMap.values.contains(folder.name) }?.map { if (it !is FileX) FileX(this@migrate, it) else it } if (downloadFolders.isNullOrEmpty()) return@launch downloadFolders.forEachIndexed { index, folder -> notification .setContentText(getString(R.string.import_old_galleries_notification_text, index, downloadFolders.size)) .setProgress(index, downloadFolders.size, false) notificationManager.notify(R.id.notification_id_import, notification.build()) val metadata = kotlin.runCatching { folder.getChild(".metadata").readText()?.let { Json.parseToJsonElement(it) } }.getOrNull() val galleryID = metadata?.getOrNull("reader")?.getOrNull("galleryInfo")?.getOrNull("id")?.content?.toIntOrNull() ?: folder.name.toIntOrNull() ?: return@forEachIndexed val galleryBlock: GalleryBlock? = kotlin.runCatching { metadata?.getOrNull("galleryBlock")?.let { Json.decodeFromJsonElement(it) } }.getOrNull() ?: kotlin.runCatching { getGalleryBlock(galleryID) }.getOrNull() ?: kotlin.runCatching { xyz.quaver.hiyobi.getGalleryBlock(galleryID) }.getOrNull() val reader: Reader? = kotlin.runCatching { metadata?.getOrNull("reader")?.let { Json.decodeFromJsonElement(it) } }.getOrNull() ?: kotlin.runCatching { getReader(galleryID) }.getOrNull() ?: kotlin.runCatching { xyz.quaver.hiyobi.getReader(galleryID) }.getOrNull() metadata?.getOrNull("thumbnail")?.jsonPrimitive?.contentOrNull?.also { thumbnail -> val file = folder.getChild(".thumbnail").also { if (it.exists()) it.delete() it.createNewFile() } file.writeBytes(Base64.decode(thumbnail, Base64.DEFAULT)) } val list: MutableList = MutableList(reader!!.galleryInfo.files.size) { null } folder.list { _, name -> name?.substringAfterLast('.') in images }?.sorted()?.take(list.size)?.forEachIndexed { i, name -> list[i] = name } folder.getChild(".metadata").also { if (it.exists()) it.delete(); it.createNewFile() }.writeText( Json.encodeToString(Metadata(galleryBlock, reader, list)) ) Cache.delete(this@migrate, galleryID) downloadFolderMap[galleryID] = folder.name downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile(); it.writeText(Json.encodeToString(downloadFolderMap)) } } notification .setContentText(getText(R.string.import_old_galleries_notification_done)) .setProgress(0, 0, false) .setOngoing(false) .mActions.clear() notificationManager.notify(R.id.notification_id_import, notification.build()) kotlin.runCatching { unregisterReceiver(receiver) } } }