From 2150d086e0ce0c18b8eac53220e862b023bb9577 Mon Sep 17 00:00:00 2001 From: tom5079 Date: Mon, 12 Jul 2021 08:44:43 +0900 Subject: [PATCH] Migrated to Ktor-client --- app/build.gradle | 18 +-- app/src/main/java/xyz/quaver/pupil/Pupil.kt | 41 +++---- .../java/xyz/quaver/pupil/sources/Common.kt | 7 +- .../java/xyz/quaver/pupil/sources/Hitomi.kt | 16 +-- .../java/xyz/quaver/pupil/ui/MainActivity.kt | 12 -- .../pupil/ui/dialog/ProxyDialogFragment.kt | 19 ++- .../ui/fragment/ManageFavoritesFragment.kt | 111 ++++++++++-------- .../ui/fragment/SourceSettingsFragment.kt | 8 +- .../java/xyz/quaver/pupil/ui/view/TagChip.kt | 8 +- .../pupil/ui/viewmodel/ReaderViewModel.kt | 15 ++- .../xyz/quaver/pupil/util/DownloadManager.kt | 3 +- .../java/xyz/quaver/pupil/util/ImageCache.kt | 91 +++++++------- .../main/java/xyz/quaver/pupil/util/misc.kt | 19 +-- .../main/java/xyz/quaver/pupil/util/proxy.kt | 12 +- .../java/xyz/quaver/pupil/util/translation.kt | 36 ++---- .../main/java/xyz/quaver/pupil/util/update.kt | 60 ---------- 16 files changed, 199 insertions(+), 277 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ab9e8835..fa929846 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -67,7 +67,11 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0-RC" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1" + + implementation "io.ktor:ktor-client-core:1.6.1" + implementation "io.ktor:ktor-client-okhttp:1.6.1" + implementation "io.ktor:ktor-client-serialization:1.6.1" implementation "androidx.appcompat:appcompat:1.3.0" implementation "androidx.activity:activity-ktx:1.3.0-rc01" @@ -83,7 +87,7 @@ dependencies { implementation "com.daimajia.swipelayout:library:1.2.0@aar" - implementation "com.google.android.material:material:1.3.0" + implementation "com.google.android.material:material:1.4.0" implementation platform("com.google.firebase:firebase-bom:28.0.0") implementation "com.google.firebase:firebase-analytics-ktx" @@ -100,8 +104,7 @@ dependencies { implementation 'com.github.piasy:FrescoImageLoader:1.8.0' implementation 'com.github.piasy:FrescoImageViewFactory:1.8.0' - //noinspection GradleDependency - implementation "com.squareup.okhttp3:okhttp:4.9.0" + implementation "org.jsoup:jsoup:1.13.1" implementation "com.tbuonomo:dotsindicator:4.2" @@ -110,15 +113,16 @@ dependencies { implementation "ru.noties.markwon:core:3.1.0" - implementation "xyz.quaver:libpupil:2.1.2" + implementation "xyz.quaver:libpupil:2.1.3" implementation "xyz.quaver:documentfilex:0.6.1" implementation "xyz.quaver:floatingsearchview:1.1.7" - implementation "com.orhanobut:logger:2.2.0" - + debugImplementation "com.orhanobut:logger:2.2.0" debugImplementation "com.squareup.leakcanary:leakcanary-android:2.6" testImplementation "junit:junit:4.13.1" + testImplementation "org.mockito:mockito-inline:3.11.2" + androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test:rules:1.4.0" androidTestImplementation "androidx.test:runner:1.4.0" diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index adee4a92..721cbddc 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -40,39 +40,39 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.ktx.Firebase import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger -import okhttp3.* +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* import org.kodein.di.* import org.kodein.di.android.x.androidXModule import xyz.quaver.io.FileX import xyz.quaver.pupil.sources.sourceModule import xyz.quaver.pupil.util.* -import xyz.quaver.setClient import java.io.File import java.util.* -lateinit var clientBuilder: OkHttpClient.Builder - -var clientHolder: OkHttpClient? = null -val client: OkHttpClient - get() = clientHolder ?: clientBuilder.build().also { - clientHolder = it - setClient(it) - } - class Pupil : Application(), DIAware { override val di: DI by DI.lazy { import(androidXModule(this@Pupil)) import(sourceModule) - bind { provider { client } } - bind { singleton { ImageCache(this@Pupil) } } - bind { singleton { DownloadManager(this@Pupil) } } + bind { singleton { ImageCache(applicationContext) } } + bind { singleton { DownloadManager(applicationContext) } } - bind(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "histories.json")) } - bind(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favorites.json")) } - bind(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "favoriteTags.json")) } - bind(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(this@Pupil), "searchHistory.json")) } + bind(tag = "histories") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "histories.json")) } + bind(tag = "favorites") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favorites.json")) } + bind(tag = "favoriteTags") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "favoriteTags.json")) } + bind(tag = "searchHistory") with singleton { SavedSourceSet(File(ContextCompat.getDataDir(applicationContext), "searchHistory.json")) } + + bind { singleton { + HttpClient(OkHttp) { + install(JsonFeature) { + serializer = KotlinxSerializer() + } + } + } } } private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -92,11 +92,6 @@ class Pupil : Application(), DIAware { Logger.addLogAdapter(AndroidLogAdapter()) - val proxyInfo = getProxyInfo() - - clientBuilder = OkHttpClient.Builder() - .proxyInfo(proxyInfo) - try { Preferences.get("download_folder").also { if (it.startsWith("content")) diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt index 0c4728ad..85ac3113 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt @@ -18,6 +18,7 @@ package xyz.quaver.pupil.sources +import io.ktor.http.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.parcelize.Parcelize @@ -129,10 +130,8 @@ abstract class Source { abstract suspend fun images(itemID: String) : List abstract suspend fun info(itemID: String) : ItemInfo - open fun getHeadersForImage(itemID: String, url: String): Map { - return emptyMap() - } - + open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { } + open fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) { binding.leftIcon.setImageResource(R.drawable.tag) } diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt index 9dd0b1f2..bc265fc3 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt @@ -20,6 +20,7 @@ package xyz.quaver.pupil.sources import android.view.LayoutInflater import android.widget.TextView +import io.ktor.http.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.parcelize.IgnoredOnParcel @@ -30,7 +31,6 @@ import xyz.quaver.hitomi.* import xyz.quaver.pupil.R import xyz.quaver.pupil.sources.ItemInfo.ExtraType import xyz.quaver.pupil.util.Preferences -import xyz.quaver.pupil.util.translations import xyz.quaver.pupil.util.wordCapitalize import kotlin.math.max import kotlin.math.min @@ -43,16 +43,18 @@ class Hitomi : Source() { } @Parcelize - data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : - SearchSuggestion { + data class TagSuggestion(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion { constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n) @IgnoredOnParcel - override val body = + override val body = s + /* + TODO if (translations[s] != null) "${translations[s]} ($s)" else s + */ } override val name: String = "hitomi.la" @@ -137,10 +139,8 @@ class Hitomi : Source() { } } - override fun getHeadersForImage(itemID: String, url: String): Map { - return mapOf( - "Referer" to getReferer(itemID.toInt()) - ) + override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { + append("Referer", getReferer(itemID.toInt())) } override fun onSuggestionBind(binding: SearchSuggestionItemBinding, item: SearchSuggestion) { 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 8d918db9..6cf44c3b 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -78,18 +78,6 @@ class MainActivity : binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) - if (intent.action == Intent.ACTION_VIEW) { - intent.dataString?.let { url -> - restore(this, url, - onFailure = { - Snackbar.make(binding.contents.recyclerview, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() - }, onSuccess = { - Snackbar.make(binding.contents.recyclerview, getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() - } - ) - } - } - if (Preferences["download_folder", ""].isEmpty()) DownloadLocationDialogFragment().show(supportFragmentManager, "Download Location Dialog") diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialogFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialogFragment.kt index 8cce55ea..1036e9f8 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialogFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialogFragment.kt @@ -19,7 +19,6 @@ package xyz.quaver.pupil.ui.dialog import android.app.Dialog -import android.content.Context import android.os.Bundle import android.view.View import android.widget.AdapterView @@ -28,18 +27,20 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.x.closestDI +import org.kodein.di.instance import xyz.quaver.pupil.R -import xyz.quaver.pupil.client -import xyz.quaver.pupil.clientBuilder -import xyz.quaver.pupil.clientHolder import xyz.quaver.pupil.databinding.ProxyDialogBinding import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.ProxyInfo import xyz.quaver.pupil.util.getProxyInfo -import xyz.quaver.pupil.util.proxyInfo import java.net.Proxy -class ProxyDialogFragment : DialogFragment() { +class ProxyDialogFragment : DialogFragment(), DIAware { + + override val di: DI by closestDI() private var _binding: ProxyDialogBinding? = null private val binding get() = _binding!! @@ -119,11 +120,7 @@ class ProxyDialogFragment : DialogFragment() { ProxyInfo(type, addr, port, username, password).let { Preferences["proxy"] = Json.encodeToString(it) - - clientBuilder - .proxyInfo(it) - clientHolder = null - client + // TODO } dismiss() diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageFavoritesFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageFavoritesFragment.kt index b685f443..42c66d3c 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageFavoritesFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/ManageFavoritesFragment.kt @@ -19,30 +19,33 @@ package xyz.quaver.pupil.ui.fragment import android.content.Intent -import android.graphics.drawable.Drawable import android.os.Bundle -import android.view.ViewGroup +import android.webkit.URLUtil import android.widget.EditText -import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.core.view.updateLayoutParams import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.snackbar.Snackbar +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import okhttp3.* import org.kodein.di.DIAware import org.kodein.di.android.x.closestDI -import org.kodein.di.android.x.di +import org.kodein.di.direct +import org.kodein.di.instance +import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.R -import xyz.quaver.pupil.client -import xyz.quaver.pupil.util.restore -import java.io.File +import xyz.quaver.pupil.util.SavedSourceSet import java.io.IOException class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware { @@ -51,6 +54,9 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware { override val di by closestDI() + private val applicationContext: Pupil by instance() + private val client: HttpClient by instance() + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.manage_favorites_preferences, rootKey) @@ -69,47 +75,43 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware { } private fun initPreferences() { - val context = context ?: return + findPreference("backup")?.setOnPreferenceClickListener { preference -> - findPreference("backup")?.setOnPreferenceClickListener { MainScope().launch { - it.icon = progressDrawable + preference.icon = progressDrawable progressDrawable.start() } - val request = Request.Builder() - .url(context.getString(R.string.backup_url)) - .post( - FormBody.Builder() - .add("f:1", File(ContextCompat.getDataDir(context), "favorites.json").readText()) - .build() - ).build() + CoroutineScope(Dispatchers.IO).launch { + kotlin.runCatching { + requireContext().openFileInput("favorites.json").use { favorites -> + val httpResponse: HttpResponse = client.submitForm( + url = "http://ix.io/", + formParameters = Parameters.build { + append("F:1", favorites.bufferedReader().readText()) + } + ) - client.newCall(request).enqueue(object: Callback { - override fun onFailure(call: Call, e: IOException) { - val view = view ?: return - Snackbar.make(view, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() - } + if (httpResponse.status.value != 200) throw IOException("Response code ${httpResponse.status.value}") - override fun onResponse(call: Call, response: Response) { - if (response.code != 200) { - response.close() - return + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, httpResponse.receive().replace("\n", "")) + }.let { + applicationContext.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share))) + } } - + }.onSuccess { MainScope().launch { progressDrawable.stop() - it.icon = null + preference.icon = null } - - Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, response.body?.use { it.string() }?.replace("\n", "")) - }.let { - getContext()?.startActivity(Intent.createChooser(it, getString(R.string.settings_backup_share))) + }.onFailure { + view?.let { + Snackbar.make(it, R.string.settings_backup_failed, Snackbar.LENGTH_LONG).show() } } - }) + } true } @@ -118,18 +120,33 @@ class ManageFavoritesFragment : PreferenceFragmentCompat(), DIAware { setText(context.getString(R.string.backup_url), TextView.BufferType.EDITABLE) } - AlertDialog.Builder(context) + AlertDialog.Builder(requireContext()) .setTitle(R.string.settings_restore_title) .setView(editText) .setPositiveButton(android.R.string.ok) { _, _ -> - restore(context, editText.text.toString(), - onFailure = onFailure@{ - val view = view ?: return@onFailure - Snackbar.make(view, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show() - }, onSuccess = onSuccess@{ - val view = view ?: return@onSuccess - Snackbar.make(view, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() - }) + CoroutineScope(Dispatchers.IO).launch { + kotlin.runCatching { + val url = editText.text.toString() + + if (!URLUtil.isValidUrl(url)) throw IllegalArgumentException() + + client.get>(url).also { + direct.instance(tag = "favorites.json").addAll(mapOf("hitomi.la" to it)) + } + }.onSuccess { + MainScope().launch { + view?.run { + Snackbar.make(this, context.getString(R.string.settings_restore_success, it.size), Snackbar.LENGTH_LONG).show() + } + } + }.onFailure { + MainScope().launch { + view?.run { + Snackbar.make(this, R.string.settings_restore_failed, Snackbar.LENGTH_LONG).show() + } + } + } + } }.setNegativeButton(android.R.string.cancel) { _, _ -> // Do Nothing }.show() diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SourceSettingsFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SourceSettingsFragment.kt index f921524b..63e132a1 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SourceSettingsFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SourceSettingsFragment.kt @@ -23,6 +23,7 @@ import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat +import io.ktor.client.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -42,8 +43,11 @@ class SourceSettingsFragment(private val source: String) : Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener, DIAware { + override val di by closestDI() + private val client: HttpClient by instance() + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(direct.instance().toMap()[source]!!, rootKey) @@ -75,7 +79,7 @@ class SourceSettingsFragment(private val source: String) : when (key) { "hitomi.tag_translation" -> { - updateTranslations() + updateTranslations(client) } else -> return false } @@ -108,7 +112,7 @@ class SourceSettingsFragment(private val source: String) : CoroutineScope(Dispatchers.IO).launch { kotlin.runCatching { - val languages = getAvailableLanguages().distinct().toTypedArray() + val languages = getAvailableLanguages(client).distinct().toTypedArray() entries = languages.map { Locale(it).let { loc -> loc.getDisplayLanguage(loc) } }.toTypedArray() entryValues = languages diff --git a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt index d9058f14..bb376453 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/view/TagChip.kt @@ -23,21 +23,21 @@ import android.content.Context import androidx.core.content.ContextCompat import com.google.android.material.chip.Chip import org.kodein.di.DIAware -import org.kodein.di.android.di +import org.kodein.di.android.closestDI import org.kodein.di.instance import xyz.quaver.pupil.R import xyz.quaver.pupil.sources.Hitomi import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.util.SavedSourceSet -import xyz.quaver.pupil.util.translations import xyz.quaver.pupil.util.wordCapitalize @SuppressLint("ViewConstructor") class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(context), DIAware { - override val di by di(context) + override val di by closestDI(context) private val favoriteTags: SavedSourceSet by instance(tag = "favoriteTags") + // TODO private val translations: Map by instance() val tag: Tag = _tag.let { @@ -94,7 +94,7 @@ class TagChip(context: Context, private val source: String, _tag: Tag) : Chip(co text = when (tag.area) { // TODO languageMap "language" -> Hitomi.languageMap[tag.tag] - else -> (translations[tag.tag] ?: tag.tag).wordCapitalize() + else -> /*(translations[tag.tag] ?: */tag.tag.wordCapitalize() } setEnsureMinTouchTargetSize(false) diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt index a04412d7..141af726 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt @@ -26,9 +26,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.orhanobut.logger.Logger +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.util.* import kotlinx.coroutines.* -import okhttp3.Headers.Companion.toHeaders -import okhttp3.Request import org.kodein.di.DIAware import org.kodein.di.android.x.closestDI import org.kodein.di.instance @@ -74,12 +75,10 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware { images.forEachIndexed { index, image -> when (val scheme = image.takeWhile { it != ':' }) { "http", "https" -> { - val file = cache.load( - Request.Builder() - .url(image) - .headers(source.getHeadersForImage(itemID, image).toHeaders()) - .build() - ) + val file = cache.load { + url(image) + headers(source.getHeadersBuilderForImage(itemID, image)) + } val channel = cache.channels[image] ?: error("Channel is null") diff --git a/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt index 8ba78510..6ba525d2 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.launch import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import okhttp3.internal.toImmutableMap import org.kodein.di.DIAware import org.kodein.di.android.closestDI import xyz.quaver.io.FileX @@ -74,7 +73,7 @@ class DownloadManager constructor(context: Context) : ContextWrapper(context), D } val downloads: Map - get() = downloadFolderMap.toImmutableMap() + get() = downloadFolderMap @Synchronized fun getDownloadFolder(source: String, itemID: String): FileX? = diff --git a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt index c9da778f..58ea3aee 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/ImageCache.kt @@ -20,24 +20,32 @@ package xyz.quaver.pupil.util import android.content.Context import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.ExperimentalCoroutinesApi +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.features.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.utils.io.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.coroutineScope -import okhttp3.* import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance +import xyz.quaver.io.FileX +import xyz.quaver.pupil.Pupil import java.io.File -import java.io.IOException import java.util.* import java.util.concurrent.ConcurrentHashMap class ImageCache(context: Context) : DIAware { override val di by closestDI(context) - private val client: OkHttpClient by instance() + private val applicationContext: Pupil by instance() + private val client: HttpClient by instance() val cacheFolder = File(context.cacheDir, "imageCache") val cache = SavedMap(File(cacheFolder, ".cache"), "", "") @@ -45,6 +53,8 @@ class ImageCache(context: Context) : DIAware { private val _channels = ConcurrentHashMap>() val channels = _channels as Map> + private val requests = mutableMapOf() + @Synchronized @OptIn(ExperimentalCoroutinesApi::class) suspend fun cleanup() = coroutineScope { @@ -62,66 +72,65 @@ class ImageCache(context: Context) : DIAware { } fun free(images: List) { - client.dispatcher.let { it.queuedCalls() + it.runningCalls() } - .filter { it.request().url.toString() in images } - .forEach { it.cancel() } + images.forEach { + requests[it]?.cancel() + } images.forEach { _channels.remove(it) } } @Synchronized suspend fun clear() = coroutineScope { - client.dispatcher.queuedCalls().forEach { it.cancel() } - + requests.values.forEach { it.cancel() } cacheFolder.listFiles()?.forEach { it.delete() } cache.clear() } @OptIn(ExperimentalCoroutinesApi::class) - fun load(request: Request): File { - val key = request.url.toString() + fun load(requestBuilder: HttpRequestBuilder.() -> Unit): File { + val request = HttpRequestBuilder().apply(requestBuilder) - val channel = if (_channels[key]?.isClosedForSend == false) + val key = request.url.buildString() + + val progressChannel = if (_channels[key]?.isClosedForSend == false) _channels[key]!! else Channel(1, BufferOverflow.DROP_OLDEST).also { _channels[key] = it } return cache[key]?.let { - channel.close() + progressChannel.close() File(it) } ?: File(cacheFolder, "${UUID.randomUUID()}.${key.takeLastWhile { it != '.' }}").also { file -> - client.newCall(request).enqueue(object: Callback { - override fun onFailure(call: Call, e: IOException) { - file.delete() - cache.remove(call.request().url.toString()) + if (!file.exists()) + file.createNewFile() - FirebaseCrashlytics.getInstance().recordException(e) - channel.close(e) - } + cache[key] = file.canonicalPath - override fun onResponse(call: Call, response: Response) { - if (response.code != 200) { - file.delete() - cache.remove(call.request().url.toString()) + requests[key] = CoroutineScope(Dispatchers.IO).launch { + kotlin.runCatching { + client.get(request).execute { httpResponse -> + val responseChannel: ByteReadChannel = httpResponse.receive() + val contentLength = httpResponse.contentLength() ?: -1 + var readBytes = 0F - channel.close(IOException("HTTP Response code is not 200")) - - response.close() - return - } - - response.body?.use { body -> - if (!file.exists()) - file.createNewFile() - - body.byteStream().copyTo(file.outputStream()) { bytes, _ -> - channel.trySendBlocking(bytes / body.contentLength().toFloat() * 100) + while (!responseChannel.isClosedForRead) { + val packet = responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong()) + while (!packet.isEmpty) { + val bytes = packet.readBytes() + file.appendBytes(bytes) + readBytes += bytes.size + progressChannel.trySend(readBytes / contentLength) + } } + progressChannel.close() } - - channel.close() + }.onFailure { + file.delete() + cache.remove(key) + FirebaseCrashlytics.getInstance().recordException(it) + progressChannel.close(it) } - }) - }.also { cache[key] = it.canonicalPath } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt index 3e744f79..c57db72d 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt @@ -23,12 +23,10 @@ import android.view.MenuItem import android.view.View import androidx.lifecycle.MutableLiveData import kotlinx.serialization.json.* -import okhttp3.OkHttpClient -import okhttp3.Request -import org.kodein.di.* -import xyz.quaver.hitomi.GalleryInfo -import xyz.quaver.hitomi.getReferer -import xyz.quaver.hitomi.imageUrlFromImage +import org.kodein.di.DIAware +import org.kodein.di.DirectDIAware +import org.kodein.di.direct +import org.kodein.di.instance import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.SourceEntries import java.io.InputStream @@ -74,13 +72,6 @@ fun byteToString(byte: Long, precision : Int = 1) : String { */ fun Int.normalizeID() = this.and(0xFFFF) -fun OkHttpClient.Builder.proxyInfo(proxyInfo: ProxyInfo) = this.apply { - proxy(proxyInfo.proxy()) - proxyInfo.authenticator()?.let { - proxyAuthenticator(it) - } -} - val formatMap = mapOf (String)>( "-id-" to { id }, "-title-" to { title }, @@ -120,7 +111,7 @@ fun MutableLiveData>.notify() { this.value = this.value } -fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Any): Long { +fun InputStream.copyTo(out: OutputStream, onCopy: (totalBytesCopied: Long, bytesJustCopied: Int) -> Unit): Long { var bytesCopied: Long = 0 val buffer = ByteArray(DEFAULT_BUFFER_SIZE) var bytes = read(buffer) diff --git a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt index e56eb365..c158ad32 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt @@ -22,8 +22,6 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import okhttp3.Authenticator -import okhttp3.Credentials import java.net.InetSocketAddress import java.net.Proxy @@ -42,15 +40,7 @@ data class ProxyInfo( Proxy(type, InetSocketAddress.createUnresolved(host, port)) } - fun authenticator(): Authenticator? = if (username.isNullOrBlank() || password.isNullOrBlank()) null else - Authenticator { _, response -> - val credential = Credentials.basic(username, password) - - response.request.newBuilder() - .header("Proxy-Authorization", credential) - .build() - } - + // TODO: Migrate to ktor-client and implement proxy authentication } fun getProxyInfo(): ProxyInfo = diff --git a/app/src/main/java/xyz/quaver/pupil/util/translation.kt b/app/src/main/java/xyz/quaver/pupil/util/translation.kt index be742259..2d307e30 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/translation.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/translation.kt @@ -18,49 +18,39 @@ package xyz.quaver.pupil.util +import io.ktor.client.* +import io.ktor.client.request.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.serialization.decodeFromString +import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Request -import xyz.quaver.pupil.client -import java.io.IOException +import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.bindInstance +import org.kodein.di.instance import java.util.* private val filesURL = "https://api.github.com/repos/tom5079/Pupil/git/trees/tags" private val contentURL = "https://raw.githubusercontent.com/tom5079/Pupil/tags/" -var translations: Map = run { - updateTranslations() - emptyMap() -} - private set +private var translations: Map = emptyMap() -@Suppress("BlockingMethodInNonBlockingContext") -fun updateTranslations() = CoroutineScope(Dispatchers.IO).launch { +fun updateTranslations(client: HttpClient) = CoroutineScope(Dispatchers.IO).launch { translations = emptyMap() kotlin.runCatching { - translations = Json.decodeFromString>(client.newCall( - Request.Builder() - .url(contentURL + "${Preferences["hitomi.tag_translation", ""].let { if (it.isEmpty()) Locale.getDefault().language else it }}.json") - .build() - ).execute().also { if (it.code != 200) return@launch }.body?.use { it.string() } ?: return@launch).filterValues { it.isNotEmpty() } + translations = client.get>("$contentURL${Preferences["hitomi.tag_translation", Locale.getDefault().language]}.json").filterValues { it.isNotEmpty() } } } -fun getAvailableLanguages(): List { +fun getAvailableLanguages(client: HttpClient): List { val languages = Locale.getISOLanguages() - val json = Json.parseToJsonElement(client.newCall( - Request.Builder() - .url(filesURL) - .build() - ).execute().also { if (it.code != 200) throw IOException() }.body?.use { it.string() } ?: return emptyList()) + val json = runCatching { runBlocking { Json.parseToJsonElement(client.get(filesURL)) } }.getOrNull() - return listOf("en") + (json["tree"]?.jsonArray?.mapNotNull { + return listOf("en") + (json?.get("tree")?.jsonArray?.mapNotNull { val name = it["path"]?.jsonPrimitive?.content?.takeWhile { c -> c != '.' } languages.firstOrNull { code -> code.equals(name, ignoreCase = true) } 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 ad41286a..132f52b5 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -19,34 +19,18 @@ package xyz.quaver.pupil.util import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent import android.net.Uri -import android.webkit.URLUtil import androidx.appcompat.app.AlertDialog -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.json.* -import okhttp3.Call -import okhttp3.Callback -import okhttp3.Request -import okhttp3.Response -import org.kodein.di.DI -import org.kodein.di.DIAware -import org.kodein.di.android.di -import org.kodein.di.instance import ru.noties.markwon.Markwon import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.R -import xyz.quaver.pupil.client import java.io.File -import java.io.IOException import java.net.URL import java.util.* @@ -178,48 +162,4 @@ fun checkUpdate(context: Context, force: Boolean = false) { dialog.show() } } -} - -fun restore(context: Context, url: String, onFailure: ((Throwable) -> Unit)? = null, onSuccess: ((Set) -> 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) { - val favorites = object: DIAware { override val di by di(context); val favorites: SavedSourceSet by instance(tag = "favorites") } - kotlin.runCatching { - Json.decodeFromString>(response.also { if (it.code != 200) throw IOException() }.body.use { it?.string() } ?: "[]").let { - favorites.favorites.addAll(mapOf("hitomi.la" to 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) - } - } - } } \ No newline at end of file