diff --git a/app/build.gradle b/app/build.gradle index f0778fdf..1e0376f8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlinx-serialization' -if (file("google-services.json").exists()) { +if (file("google-services.json").exists() && file("src/debug/google-services.json").exists()) { logger.lifecycle("Firebase Enabled") apply plugin: 'com.google.gms.google-services' apply plugin: 'io.fabric' @@ -19,19 +19,27 @@ android { applicationId "xyz.quaver.pupil" minSdkVersion 16 targetSdkVersion 29 - versionCode 41 - versionName "4.5" + versionCode 42 + versionName "4.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true vectorDrawables.useSupportLibrary = true } buildTypes { - release { - minifyEnabled false + debug { + debuggable true + applicationIdSuffix ".debug" + versionNameSuffix "-DEBUG" + + buildConfigField('Boolean', 'CENSOR', 'false') proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } - buildTypes.each { - it.buildConfigField('boolean', 'CENSOR', 'false') + release { + minifyEnabled true + shrinkResources true + + buildConfigField('Boolean', 'CENSOR', 'false') + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } kotlinOptions { @@ -57,26 +65,27 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.biometric:biometric:1.0.1" implementation 'com.android.support:multidex:1.0.3' implementation "com.daimajia.swipelayout:library:1.2.0@aar" - implementation 'com.google.android.material:material:1.2.0-alpha04' + implementation 'com.google.android.material:material:1.2.0-alpha05' implementation 'com.google.firebase:firebase-core:17.2.2' implementation 'com.google.firebase:firebase-perf:19.0.5' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.github.clans:fab:1.6.4' + implementation 'com.github.bumptech.glide:glide:4.11.0' - implementation ("com.github.bumptech.glide:recyclerview-integration:4.10.0") { + annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' + kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { transitive = false } + + implementation 'net.rdrei.android.dirchooser:library:3.2@aar' implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' implementation "ru.noties.markwon:core:${markwonVersion}" - kapt 'com.github.bumptech.glide:compiler:4.11.0' testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..5a516874 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,13 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-dontobfuscate + +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} \ No newline at end of file diff --git a/app/release/output.json b/app/release/output.json index dd112837..e58db7af 100644 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":41,"versionName":"4.5","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":42,"versionName":"4.6","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/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..944c7613 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + Pupil-Debug + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d445644f..f9bf5296 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,8 +6,8 @@ - + + + tools:replace="android:theme" + android:requestLegacyExternalStorage="true"> () { + val glide = Glide.with(context) + //region Glide.RecyclerView - inner class SizeProvider : ListPreloader.PreloadSizeProvider { - - override fun getPreloadSize(item: File, adapterPosition: Int, itemPosition: Int): IntArray? { - return Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.getOrNull(itemPosition)?.let { - arrayOf(it.width, it.height).toIntArray() - } + val sizeProvider = ListPreloader.PreloadSizeProvider { _, _, position -> + Cache(context).getReaderOrNull(galleryID)?.galleryInfo?.files?.getOrNull(position)?.let { + arrayOf(it.width, it.height).toIntArray() } - } - - inner class ModelProvider : ListPreloader.PreloadModelProvider { - + val modelProvider = object: ListPreloader.PreloadModelProvider { override fun getPreloadItems(position: Int): MutableList { - return listOf(Cache(context).getImages(galleryID)?.get(position)).filterNotNullTo(mutableListOf()) + return listOf(Cache(context).getImages(galleryID)?.getOrNull(position)).filterNotNullTo(mutableListOf()) } override fun getPreloadRequestBuilder(item: File): RequestBuilder<*>? { @@ -76,31 +72,17 @@ class ReaderAdapter(private val context: Context, override(5, 8) } } - } + val preloader = RecyclerViewPreloader(glide, modelProvider, sizeProvider, 10) //endregion var reader: Reader? = null - val glide = Glide.with(context) val timer = Timer() - val sizeProvider = SizeProvider() - val modelProvider = ModelProvider() - val preloader = RecyclerViewPreloader(glide, modelProvider, sizeProvider, 10) - var isFullScreen = false var onItemClickListener : ((Int) -> (Unit))? = null - init { - CoroutineScope(Dispatchers.IO).launch { - reader = Cache(context).getReader(galleryID) - launch(Dispatchers.Main) { - notifyDataSetChanged() - } - } - } - class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -114,10 +96,13 @@ class ReaderAdapter(private val context: Context, override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.view as ConstraintLayout - if (isFullScreen) + if (isFullScreen) { holder.view.layoutParams.height = RecyclerView.LayoutParams.MATCH_PARENT - else + holder.view.container.layoutParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT + } else { holder.view.layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT + holder.view.container.layoutParams.height = 0 + } holder.view.image.setOnPhotoTapListener { _, _, _ -> onItemClickListener?.invoke(position) @@ -127,57 +112,55 @@ class ReaderAdapter(private val context: Context, onItemClickListener?.invoke(position) } - (holder.view.container.layoutParams as ConstraintLayout.LayoutParams) - .dimensionRatio = "${reader!!.galleryInfo[position].width}:${reader!!.galleryInfo[position].height}" + if (!isFullScreen) + (holder.view.container.layoutParams as ConstraintLayout.LayoutParams) + .dimensionRatio = "${reader!!.galleryInfo.files[position].width}:${reader!!.galleryInfo.files[position].height}" holder.view.reader_index.text = (position+1).toString() - CoroutineScope(Dispatchers.IO).launch { - val images = Cache(context).getImages(galleryID) + val images = Cache(context).getImage(galleryID, position) - launch(Dispatchers.Main) { - if (images?.get(position) != null) { - glide - .load(images[position]) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .error(R.drawable.image_broken_variant) - .apply { - if (BuildConfig.CENSOR) - override(5, 8) - } - .into(holder.view.image) - } else { - val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) + if (images != null) { + glide + .load(images) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .error(R.drawable.image_broken_variant) + .apply { + if (BuildConfig.CENSOR) + override(5, 8) + } + .into(holder.view.image) + } else { + val progress = DownloadWorker.getInstance(context).progress[galleryID]?.get(position) - if (progress?.isNaN() == true) { + if (progress?.isNaN() == true) { + if (Fabric.isInitialized()) + Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) - if (Fabric.isInitialized()) - Crashlytics.logException(DownloadWorker.getInstance(context).exception[galleryID]?.get(position)) + glide + .load(R.drawable.image_broken_variant) + .into(holder.view.image) - glide - .load(R.drawable.image_broken_variant) - .into(holder.view.image) - } else { - holder.view.reader_item_progressbar.progress = - if (progress?.isInfinite() == true) - 100 - else - progress?.roundToInt() ?: 0 + return + } else { + holder.view.reader_item_progressbar.progress = + if (progress?.isInfinite() == true) + 100 + else + progress?.roundToInt() ?: 0 - holder.view.image.setImageDrawable(null) - } + holder.view.image.setImageDrawable(null) + } - timer.schedule(1000) { - CoroutineScope(Dispatchers.Main).launch { - notifyItemChanged(position) - } - } + timer.schedule(1000) { + CoroutineScope(Dispatchers.Main).launch { + notifyItemChanged(position) } } } } - override fun getItemCount() = reader?.galleryInfo?.size ?: 0 + override fun getItemCount() = reader?.galleryInfo?.files?.size ?: 0 } \ No newline at end of file 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 d5f33096..97f50840 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt @@ -45,15 +45,13 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion import com.arlib.floatingsearchview.util.view.SearchInputView +import com.crashlytics.android.Crashlytics import com.google.android.material.appbar.AppBarLayout +import io.fabric.sdk.android.Fabric import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main_content.* import kotlinx.coroutines.* -import kotlinx.serialization.ImplicitReflectionSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.list -import kotlinx.serialization.stringify import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.doSearch import xyz.quaver.hitomi.getGalleryIDsFromNozomi @@ -179,7 +177,7 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() - (main_recyclerview.adapter as GalleryBlockAdapter).timer.cancel() + (main_recyclerview?.adapter as? GalleryBlockAdapter)?.timer?.cancel() } override fun onResume() { @@ -693,7 +691,6 @@ class MainActivity : AppCompatActivity() { } private var suggestionJob : Job? = null - @UseExperimental(ImplicitReflectionSerializer::class) private fun setupSearchBar() { val searchInputView = findViewById(R.id.search_bar_text) //Change upper case letters to lower case @@ -717,12 +714,11 @@ class MainActivity : AppCompatActivity() { with(main_searchview as FloatingSearchView) { val favoritesFile = File(ContextCompat.getDataDir(context), "favorites_tags.json") - val json = Json(JsonConfiguration.Stable) val serializer = Tag.serializer().list if (!favoritesFile.exists()) { favoritesFile.createNewFile() - favoritesFile.writeText(json.stringify(Tags(listOf()))) + favoritesFile.writeText(json.stringify(serializer, Tags(listOf()))) } setOnMenuItemClickListener { @@ -840,7 +836,7 @@ class MainActivity : AppCompatActivity() { favorites.add(tag) } - favoritesFile.writeText(json.stringify(favorites)) + favoritesFile.writeText(json.stringify(serializer, favorites)) } } @@ -939,26 +935,26 @@ class MainActivity : AppCompatActivity() { when(sortMode) { SortMode.POPULAR -> getGalleryIDsFromNozomi(null, "popular", "all") else -> getGalleryIDsFromNozomi(null, "index", "all") - }.apply { - totalItems = size + }.also { + totalItems = it.size } } - else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).apply { - totalItems = size + else -> doSearch("$defaultQuery $query", sortMode == SortMode.POPULAR).also { + totalItems = it.size } } } Mode.HISTORY -> { when { query.isEmpty() -> { - histories.toList().apply { - totalItems = size + histories.toList().also { + totalItems = it.size } } else -> { val result = doSearch(query).sorted() - histories.filter { result.binarySearch(it) >= 0 }.apply { - totalItems = size + histories.filter { result.binarySearch(it) >= 0 }.also { + totalItems = it.size } } } @@ -971,26 +967,26 @@ class MainActivity : AppCompatActivity() { } ?: emptyList() when { - query.isEmpty() -> downloads.apply { - totalItems = size + query.isEmpty() -> downloads.also { + totalItems = it.size } else -> { val result = doSearch(query).sorted() - downloads.filter { result.binarySearch(it) >= 0 }.apply { - totalItems = size + downloads.filter { result.binarySearch(it) >= 0 }.also { + totalItems = it.size } } } } Mode.FAVORITE -> { when { - query.isEmpty() -> favorites.toList().apply { - totalItems = size + query.isEmpty() -> favorites.toList().also { + totalItems = it.size } else -> { val result = doSearch(query).sorted() - favorites.filter { result.binarySearch(it) >= 0 }.apply { - totalItems = size + favorites.filter { result.binarySearch(it) >= 0 }.also { + totalItems = it.size } } } @@ -1004,9 +1000,16 @@ class MainActivity : AppCompatActivity() { val perPage = preference.getString("per_page", "25")?.toInt() ?: 25 loadingJob = CoroutineScope(Dispatchers.IO).launch { - val galleryIDs = galleryIDs?.await() + val galleryIDs = try { + galleryIDs!!.await().also { + if (it.isEmpty()) + throw Exception("No result") + } + } catch (e: Exception) { + + if (Fabric.isInitialized() && e.message != "No result") + Crashlytics.logException(e) - if (galleryIDs.isNullOrEmpty()) { //No result withContext(Dispatchers.Main) { main_noresult.visibility = View.VISIBLE main_progressbar.hide() diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt index 7af22aec..25fa0e4f 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt @@ -38,7 +38,6 @@ import io.fabric.sdk.android.Fabric import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.view.* import kotlinx.android.synthetic.main.dialog_numberpicker.view.* -import kotlinx.serialization.ImplicitReflectionSerializer import xyz.quaver.Code import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.R @@ -141,7 +140,6 @@ class ReaderActivity : AppCompatActivity() { super.onResume() } - @UseExperimental(ImplicitReflectionSerializer::class) override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.reader, menu) @@ -256,11 +254,17 @@ class ReaderActivity : AppCompatActivity() { reader_progressbar.max = reader_recyclerview.adapter?.itemCount ?: 0 if (title == getString(R.string.reader_loading)) { - val reader = (reader_recyclerview.adapter as ReaderAdapter).reader + val reader = Cache(this@ReaderActivity).getReaderOrNull(galleryID) if (reader != null) { - title = reader.title - menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.size}" + + with (reader_recyclerview.adapter as ReaderAdapter) { + this.reader = reader + notifyDataSetChanged() + } + + title = reader.galleryInfo.title + menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${reader.galleryInfo.files.size}" menu?.findItem(R.id.reader_type)?.icon = ContextCompat.getDrawable(this@ReaderActivity, when (reader.code) { @@ -296,7 +300,7 @@ class ReaderActivity : AppCompatActivity() { } } - //addOnScrollListener((adapter as ReaderAdapter).preloader) + addOnScrollListener((adapter as ReaderAdapter).preloader) addOnScrollListener(object: RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) 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 aa09d8a4..ebcdbb77 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/SettingsActivity.kt @@ -30,9 +30,8 @@ import androidx.appcompat.app.AppCompatActivity 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 kotlinx.serialization.list +import kotlinx.serialization.serializer import net.rdrei.android.dirchooser.DirectoryChooserActivity import xyz.quaver.pupil.Pupil import xyz.quaver.pupil.R @@ -79,7 +78,6 @@ class SettingsActivity : AppCompatActivity() { return true } - @UseExperimental(ImplicitReflectionSerializer::class) override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when(requestCode) { REQUEST_LOCK -> { @@ -96,13 +94,13 @@ class SettingsActivity : AppCompatActivity() { val uri = data?.data ?: return try { - val json = contentResolver.openInputStream(uri).use { inputStream -> + val str = contentResolver.openInputStream(uri).use { inputStream -> inputStream!! inputStream.readBytes().toString(Charset.defaultCharset()) } - (application as Pupil).favorites.addAll(Json.parseList(json).also { + (application as Pupil).favorites.addAll(json.parse(Int.serializer().list, str).also { Snackbar.make( window.decorView, getString(R.string.settings_restore_successful, it.size), diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DefaultQueryDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DefaultQueryDialog.kt index 125fa48b..3d39c6e4 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/DefaultQueryDialog.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/DefaultQueryDialog.kt @@ -46,16 +46,12 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) { private val excludeBL = "-male:yaoi" private val excludeGuro = listOf("-female:guro", "-male:guro") - private lateinit var dialogView : View - var onPositiveButtonClickListener : ((Tags) -> (Unit))? = null @SuppressLint("InflateParams") override fun onCreate(savedInstanceState: Bundle?) { - initDialog() - setTitle(R.string.default_query_dialog_title) - setView(dialogView) + setView(build()) setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> val newTags = Tags.parse(default_query_dialog_edittext.text.toString()) @@ -79,15 +75,15 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) { } @SuppressLint("InflateParams") - private fun initDialog() { + private fun build() : View { val preferences = PreferenceManager.getDefaultSharedPreferences(context) val tags = Tags.parse( preferences.getString("default_query", "") ?: "" ) - dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null) + val view = LayoutInflater.from(context).inflate(R.layout.dialog_default_query, null) - with(dialogView.default_query_dialog_language_selector) { + with(view.default_query_dialog_language_selector) { adapter = ArrayAdapter( context, @@ -110,13 +106,13 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) { } } - with(dialogView.default_query_dialog_BL_checkbox) { + with(view.default_query_dialog_BL_checkbox) { isChecked = tags.contains(excludeBL) if (tags.contains(excludeBL)) tags.remove(excludeBL) } - with(dialogView.default_query_dialog_guro_checkbox) { + with(view.default_query_dialog_guro_checkbox) { isChecked = excludeGuro.all { tags.contains(it) } if (excludeGuro.all { tags.contains(it) }) excludeGuro.forEach { @@ -124,7 +120,7 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) { } } - with(dialogView.default_query_dialog_edittext) { + with(view.default_query_dialog_edittext) { setText(tags.toString(), android.widget.TextView.BufferType.EDITABLE) addTextChangedListener(object : TextWatcher { override fun beforeTextChanged( @@ -149,6 +145,8 @@ class DefaultQueryDialog(context : Context) : AlertDialog(context) { } }) } + + return view } } \ No newline at end of file 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 3f24f0b0..3de0c7d7 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 @@ -26,6 +26,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.view.View import android.widget.LinearLayout import android.widget.RadioButton import androidx.appcompat.app.AlertDialog @@ -36,10 +37,7 @@ 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.REQUEST_WRITE_PERMISSION_AND_SAF -import xyz.quaver.pupil.util.byteToString +import xyz.quaver.pupil.util.* import java.io.File @SuppressLint("InflateParams") @@ -49,6 +47,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) { private val buttons = mutableListOf>() override fun onCreate(savedInstanceState: Bundle?) { + setTitle(R.string.settings_dl_location) + + setView(build()) + + setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> } + + super.onCreate(savedInstanceState) + } + + private fun build() : View { val view = layoutInflater.inflate(R.layout.dialog_dl_location, null) as LinearLayout val externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null) @@ -115,25 +123,16 @@ class DownloadLocationDialog(val activity: Activity) : AlertDialog(activity) { buttons.add(button to null) }) - val pref = preference.getString("dl_location", null) - val index = externalFilesDirs.indexOfFirst { - it.canonicalPath == pref + externalFilesDirs.indexOfFirst { + it.canonicalPath == getDownloadDirectory(context).canonicalPath + }.let { index -> + if (index < 0) + buttons.first().first.isChecked = true + else + buttons[index].first.isChecked = true } - if (index < 0) - buttons.last().first.isChecked = true - else - buttons[index].first.isChecked = true - - setTitle(R.string.settings_dl_location) - - setView(view) - - setButton(Dialog.BUTTON_POSITIVE, context.getText(android.R.string.ok)) { _, _ -> - dismiss() - } - - super.onCreate(savedInstanceState) + return view } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt index 1a2f0351..d20b61df 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/MirrorDialog.kt @@ -22,6 +22,7 @@ import android.annotation.SuppressLint import android.app.Dialog import android.content.Context import android.os.Bundle +import android.view.View import androidx.appcompat.app.AlertDialog import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration @@ -56,21 +57,17 @@ class MirrorDialog(context: Context) : AlertDialog(context) { } } - private lateinit var recyclerView: RecyclerView - @SuppressLint("InflateParams") override fun onCreate(savedInstanceState: Bundle?) { - initDialog() - setTitle(R.string.settings_mirror_title) - setView(recyclerView) + setView(build()) setButton(Dialog.BUTTON_POSITIVE, context.getString(android.R.string.ok)) { _, _ -> } super.onCreate(savedInstanceState) } - private fun initDialog() { - recyclerView = RecyclerView(context).apply recyclerview@{ + private fun build() : View { + return RecyclerView(context).apply recyclerview@{ addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) layoutManager = LinearLayoutManager(context) adapter = MirrorAdapter(context).apply adapter@{ diff --git a/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt new file mode 100644 index 00000000..68eb1e0a --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/dialog/ProxyDialog.kt @@ -0,0 +1,133 @@ +/* + * 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.ui.dialog + +import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.preference.PreferenceManager +import kotlinx.android.synthetic.main.dialog_proxy.view.* +import xyz.quaver.proxy +import xyz.quaver.pupil.R +import xyz.quaver.pupil.util.ProxyInfo +import xyz.quaver.pupil.util.getProxyInfo +import xyz.quaver.pupil.util.json +import java.net.Proxy + +class ProxyDialog(context: Context) : Dialog(context) { + + override fun onCreate(savedInstanceState: Bundle?) { + val view = build() + + setTitle(R.string.settings_proxy_title) + setContentView(view) + + window?.attributes?.width = ViewGroup.LayoutParams.MATCH_PARENT + + super.onCreate(savedInstanceState) + } + + @SuppressLint("InflateParams") + private fun build() : View { + val proxyInfo = getProxyInfo(context) + + val view = LayoutInflater.from(context).inflate(R.layout.dialog_proxy, null) + + val enabler = { enable: Boolean -> + view?.proxy_addr?.isEnabled = enable + view?.proxy_port?.isEnabled = enable + view?.proxy_username?.isEnabled = enable + view?.proxy_password?.isEnabled = enable + + if (!enable) { + view?.proxy_addr?.text = null + view?.proxy_port?.text = null + view?.proxy_username?.text = null + view?.proxy_password?.text = null + } + } + + with(view.proxy_type_selector) { + adapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, + context.resources.getStringArray(R.array.proxy_type) + ) + + setSelection(proxyInfo.type.ordinal) + + onItemSelectedListener = object: AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + enabler.invoke(position != 0) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + } + + view.proxy_addr.setText(proxyInfo.host) + view.proxy_port.setText(proxyInfo.port?.toString()) + view.proxy_username.setText(proxyInfo.username) + view.proxy_password.setText(proxyInfo.password) + + enabler.invoke(proxyInfo.type != Proxy.Type.DIRECT) + + view.proxy_cancel.setOnClickListener { + dismiss() + } + + view.proxy_ok.setOnClickListener { + val type = Proxy.Type.values()[view.proxy_type_selector.selectedItemPosition] + val addr = view.proxy_addr.text?.toString() + val port = view.proxy_port.text?.toString()?.toIntOrNull() + val username = view.proxy_username.text?.toString() + val password = view.proxy_password.text?.toString() + + if (type != Proxy.Type.DIRECT) { + if (addr == null || addr.isEmpty()) + view.proxy_addr.error = context.getText(R.string.proxy_dialog_error) + if (port == null) + view.proxy_port.error = context.getText(R.string.proxy_dialog_error) + + if (addr == null || addr.isEmpty() || port == null) + return@setOnClickListener + } + + ProxyInfo(type, addr, port, username, password).let { + + PreferenceManager.getDefaultSharedPreferences(context).edit().putString("proxy", + json.stringify(ProxyInfo.serializer(), it) + ).apply() + + proxy = it.proxy() + } + + dismiss() + } + + return view + } + +} \ No newline at end of file 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 f3d88dad..3a3bcdbd 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 @@ -36,6 +36,7 @@ import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.dialog.DefaultQueryDialog import xyz.quaver.pupil.ui.dialog.DownloadLocationDialog import xyz.quaver.pupil.ui.dialog.MirrorDialog +import xyz.quaver.pupil.ui.dialog.ProxyDialog import xyz.quaver.pupil.util.* import java.io.File @@ -146,6 +147,10 @@ class SettingsFragment : MirrorDialog(context) .show() } + "proxy" -> { + ProxyDialog(context) + .show() + } "backup" -> { File(ContextCompat.getDataDir(context), "favorites.json").copyTo( File(getDownloadDirectory(context), "favorites.json"), @@ -189,9 +194,18 @@ class SettingsFragment : } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - when (key) { - "dl_location" -> { - findPreference(key)?.summary = getDownloadDirectory(context!!).canonicalPath + key ?: return + + with(findPreference(key)) { + this ?: return + + when (key) { + "proxy" -> { + summary = getProxyInfo(context).type.name + } + "dl_location" -> { + summary = getDownloadDirectory(context!!).canonicalPath + } } } } @@ -245,8 +259,7 @@ class SettingsFragment : onPreferenceClickListener = this@SettingsFragment } "default_query" -> { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - summary = preferences.getString("default_query", "") ?: "" + summary = PreferenceManager.getDefaultSharedPreferences(context).getString("default_query", "") ?: "" onPreferenceClickListener = this@SettingsFragment } @@ -270,6 +283,11 @@ class SettingsFragment : "mirrors" -> { onPreferenceClickListener = this@SettingsFragment } + "proxy" -> { + summary = getProxyInfo(context).type.name + + onPreferenceClickListener = this@SettingsFragment + } "dark_mode" -> { onPreferenceChangeListener = 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 index fecea791..38b24cf2 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/ConstValues.kt @@ -18,8 +18,13 @@ package xyz.quaver.pupil.util +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration + const val REQUEST_LOCK = 38238 const val REQUEST_RESTORE = 16546 const val REQUEST_DOWNLOAD_FOLDER = 3874 const val REQUEST_DOWNLOAD_FOLDER_OLD = 3425 -const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900 \ No newline at end of file +const val REQUEST_WRITE_PERMISSION_AND_SAF = 13900 + +val json = Json(JsonConfiguration.Stable) \ 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 6c75b55d..a4808a9a 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,21 +21,18 @@ package xyz.quaver.pupil.util.download import android.content.Context import android.content.ContextWrapper import android.util.Base64 -import android.util.Log import androidx.preference.PreferenceManager +import com.crashlytics.android.Crashlytics import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import kotlinx.serialization.ImplicitReflectionSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.parse -import kotlinx.serialization.stringify import xyz.quaver.Code import xyz.quaver.hitomi.GalleryBlock import xyz.quaver.hitomi.Reader import xyz.quaver.pupil.util.getCachedGallery import xyz.quaver.pupil.util.getDownloadDirectory +import xyz.quaver.pupil.util.json import java.io.File import java.net.URL @@ -50,7 +47,6 @@ class Cache(context: Context) : ContextWrapper(context) { it.mkdirs() } - @UseExperimental(ImplicitReflectionSerializer::class) fun getCachedMetadata(galleryID: Int) : Metadata? { val file = File(getCachedGallery(galleryID), ".metadata") @@ -58,7 +54,7 @@ class Cache(context: Context) : ContextWrapper(context) { return null return try { - Json.parse(file.readText()) + json.parse(Metadata.serializer(), file.readText()) } catch (e: Exception) { //File corrupted file.delete() @@ -66,14 +62,13 @@ class Cache(context: Context) : ContextWrapper(context) { } } - @UseExperimental(ImplicitReflectionSerializer::class) fun setCachedMetadata(galleryID: Int, metadata: Metadata) { val file = File(getCachedGallery(galleryID), ".metadata").also { if (!it.exists()) it.createNewFile() } - file.writeText(Json.stringify(metadata)) + file.writeText(json.stringify(Metadata.serializer(), metadata)) } suspend fun getThumbnail(galleryID: Int): String? { @@ -102,21 +97,29 @@ class Cache(context: Context) : ContextWrapper(context) { suspend fun getGalleryBlock(galleryID: Int): GalleryBlock? { val metadata = Cache(this).getCachedMetadata(galleryID) - val source = mapOf( - Code.HITOMI to { xyz.quaver.hitomi.getGalleryBlock(galleryID) }, - Code.HIYOBI to { xyz.quaver.hiyobi.getGalleryBlock(galleryID) } + val sources = listOf( + { xyz.quaver.hitomi.getGalleryBlock(galleryID) }, + { xyz.quaver.hiyobi.getGalleryBlock(galleryID) } ) - val galleryBlock = if (metadata?.galleryBlock == null) - source.entries.map { - CoroutineScope(Dispatchers.IO).async { - kotlin.runCatching { - it.value.invoke() - }.getOrNull() - } - }.firstOrNull { - it.await() != null - }?.await() + val galleryBlock = if (metadata?.galleryBlock == null) { + CoroutineScope(Dispatchers.IO).async { + var galleryBlock: GalleryBlock? = null + + for (source in sources) { + galleryBlock = try { + source.invoke() + } catch (e: Exception) { + null + } + + if (galleryBlock != null) + break + } + + galleryBlock + }.await() ?: return null + } else metadata.galleryBlock @@ -155,40 +158,60 @@ class Cache(context: Context) : ContextWrapper(context) { var retval: Reader? = null for (source in sources) { - retval = kotlin.runCatching { + retval = try { source.value.invoke() - }.getOrNull() + } catch (e: Exception) { + Crashlytics.logException(e) + null + } if (retval != null) break } retval - }.await() + }.await() ?: return null } else metadata.reader - if (reader != null) - setCachedMetadata( - galleryID, - Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader) - ) + setCachedMetadata( + galleryID, + Metadata(Cache(this).getCachedMetadata(galleryID), readers = reader) + ) return reader } + val imageNameRegex = Regex("""^\d+\..+$""") fun getImages(galleryID: Int): List? { - val started = System.currentTimeMillis() val gallery = getCachedGallery(galleryID) - val reader = getReaderOrNull(galleryID) ?: return null - val images = gallery.listFiles() ?: return null - Log.i("PUPILD", "${System.currentTimeMillis() - started} ms") - return reader.galleryInfo.indices.map { index -> - images.firstOrNull { file -> file.name.startsWith("%05d".format(index)) } + return gallery.list { _, name -> + imageNameRegex.matches(name) + }?.map { + File(gallery, it) } } + val imageExtensions = listOf( + "png", + "jpg", + "webp", + "gif" + ) + fun getImage(galleryID: Int, index: Int): File? { + val gallery = getCachedGallery(galleryID) + + for (ext in imageExtensions) { + File(gallery, "%05d.$ext".format(index)).let { + if (it.exists()) + return it + } + } + + return null + } + fun putImage(galleryID: Int, name: String, data: ByteArray) { val cache = File(getCachedGallery(galleryID), name).also { if (!it.exists()) 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 aa5dda63..bb3a2716 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 @@ -40,6 +40,7 @@ import xyz.quaver.hitomi.urlFromUrlFromHash import xyz.quaver.hiyobi.cookie import xyz.quaver.hiyobi.createImgList import xyz.quaver.hiyobi.user_agent +import xyz.quaver.proxy import xyz.quaver.pupil.R import xyz.quaver.pupil.ui.ReaderActivity import java.io.IOException @@ -145,25 +146,22 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont private val loop = loop() private val worker = SparseArray() - @Volatile var nRunners = 0 + val clients = SparseArray() - private val client = OkHttpClient.Builder() - .addInterceptor { chain -> - val request = chain.request() - var response = chain.proceed(request) + val interceptor = Interceptor { chain -> + val request = chain.request() + val response = chain.proceed(request) - var retry = preferences.getInt("retry", 3) - while (!response.isSuccessful && retry > 0) { - response = chain.proceed(request) - retry-- - } - - response.newBuilder() - .body(ProgressResponseBody(request.tag(), response.body(), progressListener)) - .build() - } - .dispatcher(Dispatcher(Executors.newFixedThreadPool(4))) + response.newBuilder() + .body(ProgressResponseBody(request.tag(), response.body(), progressListener)) .build() + } + fun buildClient() = + OkHttpClient.Builder() + .addInterceptor(interceptor) + .dispatcher(Dispatcher(Executors.newFixedThreadPool(4))) + .proxy(proxy) + .build() fun stop() { queue.clear() @@ -176,29 +174,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont worker[galleryID]?.cancel() } - client.dispatcher().cancelAll() + for (i in 0 until clients.size()) { + clients.valueAt(i).dispatcher().cancelAll() + } + clients.clear() progress.clear() exception.clear() notification.clear() notificationManager.cancelAll() - - nRunners = 0 - } fun cancel(galleryID: Int) { queue.remove(galleryID) worker[galleryID]?.cancel() - client.dispatcher().queuedCalls() - .filter { - @Suppress("UNCHECKED_CAST") - (it.request().tag() as? Pair)?.first == galleryID - } - .forEach { - it.cancel() - } + clients[galleryID]?.dispatcher()?.cancelAll() + clients.remove(galleryID) progress.remove(galleryID) exception.remove(galleryID) @@ -207,7 +199,6 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont if (progress.indexOfKey(galleryID) >= 0) { Cache(this@DownloadWorker).setDownloading(galleryID, false) - nRunners-- } } @@ -222,7 +213,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont url( urlFromUrlFromHash( galleryID, - reader.galleryInfo[index], + reader.galleryInfo.files[index], if (lowQuality) "webp" else null ) ) @@ -240,7 +231,10 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont tag(galleryID to index) }.build() - client.newCall(request).enqueue(callback) + if (clients.get(galleryID) == null) + clients.put(galleryID, buildClient()) + + clients[galleryID]?.newCall(request)?.enqueue(callback) } private fun download(galleryID: Int) = CoroutineScope(Dispatchers.IO).launch { @@ -252,24 +246,23 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont exception.put(galleryID, null) Cache(this@DownloadWorker).setDownloading(galleryID, false) - nRunners-- return@launch } val cache = Cache(this@DownloadWorker).getImages(galleryID) - progress.put(galleryID, reader.galleryInfo.indices.map { index -> - if (cache?.get(index) != null) + progress.put(galleryID, reader.galleryInfo.files.indices.map { index -> + if (cache?.getOrNull(index) != null) Float.POSITIVE_INFINITY else 0F }.toMutableList()) - exception.put(galleryID, reader.galleryInfo.map { null }.toMutableList()) + exception.put(galleryID, reader.galleryInfo.files.map { null }.toMutableList()) if (notification[galleryID] == null) initNotification(galleryID) - notification[galleryID].setContentTitle(reader.title) + notification[galleryID].setContentTitle(reader.galleryInfo.title) notify(galleryID) if (isCompleted(galleryID)) { @@ -279,15 +272,14 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont setDownloading(galleryID, false) } } - nRunners-- return@launch } - for (i in reader.galleryInfo.indices) { + for (i in reader.galleryInfo.files.indices) { val callback = object : Callback { override fun onFailure(call: Call, e: IOException) { - if (Fabric.isInitialized()) + if (Fabric.isInitialized() && e.message != "Canceled") Crashlytics.logException(e) progress[galleryID]?.set(i, Float.NaN) @@ -302,7 +294,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont setDownloading(galleryID, false) } } - nRunners-- + clients.remove(galleryID) } } @@ -325,7 +317,7 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont setDownloading(galleryID, false) } } - nRunners-- + clients.remove(galleryID) } } } @@ -342,7 +334,9 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont if (isCompleted(galleryID)) notification[galleryID] ?.setContentText(getString(R.string.reader_notification_complete)) + ?.setSmallIcon(android.R.drawable.stat_sys_download_done) ?.setProgress(0, 0, false) + ?.setOngoing(false) else notification[galleryID] ?.setProgress(max, progress, false) @@ -369,24 +363,24 @@ class DownloadWorker private constructor(context: Context) : ContextWrapper(cont setSmallIcon(android.R.drawable.stat_sys_download) // had to use this because old android doesn't support VectorDrawable on Notification :P setContentIntent(pendingIntent) setProgress(0, 0, true) + setOngoing(true) }) } private fun loop() = CoroutineScope(Dispatchers.Default).launch { while (true) { - if (queue.isEmpty() || nRunners > preferences.getInt("max_download", 4)) + if (queue.isEmpty() || clients.size() > preferences.getInt("max_download", 4)) continue val galleryID = queue.poll() ?: continue - if (progress.indexOfKey(galleryID) >= 0) // Gallery already downloading! + if (clients.indexOfKey(galleryID) >= 0) // Gallery already downloading! continue initNotification(galleryID) if (Cache(this@DownloadWorker).isDownloading(galleryID)) notificationManager.notify(galleryID, notification[galleryID].build()) worker.put(galleryID, download(galleryID)) - nRunners++ } } diff --git a/app/src/main/java/xyz/quaver/pupil/util/history.kt b/app/src/main/java/xyz/quaver/pupil/util/history.kt index 665ccdfe..cb13c226 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/history.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/history.kt @@ -18,15 +18,14 @@ package xyz.quaver.pupil.util -import kotlinx.serialization.ImplicitReflectionSerializer -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonConfiguration -import kotlinx.serialization.parseList -import kotlinx.serialization.stringify +import kotlinx.serialization.list +import kotlinx.serialization.serializer import java.io.File class Histories(private val file: File) : ArrayList() { + val serializer = Int.serializer().list + init { if (!file.exists()) file.parentFile?.mkdirs() @@ -38,21 +37,20 @@ class Histories(private val file: File) : ArrayList() { } } - @UseExperimental(ImplicitReflectionSerializer::class) fun load() : Histories { return apply { super.clear() addAll( - Json(JsonConfiguration.Stable).parseList( + json.parse( + serializer, file.bufferedReader().use { it.readText() } ) ) } } - @UseExperimental(ImplicitReflectionSerializer::class) fun save() { - file.writeText(Json(JsonConfiguration.Stable).stringify(this)) + file.writeText(json.stringify(serializer, this)) } override fun add(element: Int): Boolean { diff --git a/app/src/main/java/xyz/quaver/pupil/util/lock.kt b/app/src/main/java/xyz/quaver/pupil/util/lock.kt index cba085c4..4f48b784 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/lock.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/lock.kt @@ -21,9 +21,10 @@ package xyz.quaver.pupil.util import android.content.Context import android.content.ContextWrapper import androidx.core.content.ContextCompat -import kotlinx.serialization.* +import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration +import kotlinx.serialization.list import java.io.File import java.security.MessageDigest @@ -73,7 +74,6 @@ class LockManager(base: Context): ContextWrapper(base) { load() } - @UseExperimental(ImplicitReflectionSerializer::class) private fun load() { val lock = File(ContextCompat.getDataDir(this), "lock.json") @@ -82,17 +82,16 @@ class LockManager(base: Context): ContextWrapper(base) { lock.writeText("[]") } - locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText())) + locks = ArrayList(json.parse(Lock.serializer().list, lock.readText())) } - @UseExperimental(ImplicitReflectionSerializer::class) private fun save() { val lock = File(ContextCompat.getDataDir(this), "lock.json") if (!lock.exists()) lock.createNewFile() - lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf())) + lock.writeText(json.stringify(Lock.serializer().list, locks?.toList() ?: listOf())) } fun add(lock: Lock) { diff --git a/app/src/main/java/xyz/quaver/pupil/util/proxy.kt b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt new file mode 100644 index 00000000..229c5e90 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/util/proxy.kt @@ -0,0 +1,63 @@ +/* + * 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.content.Context +import androidx.preference.PreferenceManager +import kotlinx.serialization.Serializable +import okhttp3.Authenticator +import okhttp3.Credentials +import java.net.InetSocketAddress +import java.net.Proxy + +@Serializable +data class ProxyInfo( + val type: Proxy.Type, + val host: String? = null, + val port: Int? = null, + val username: String? = null, + val password: String? = null +) { + fun proxy() : Proxy { + return if (host == null || port == null) + return Proxy.NO_PROXY + else + Proxy(type, InetSocketAddress.createUnresolved(host, port)) + } + + fun authenticator() = Authenticator { _, response -> + val credential = Credentials.basic(username, password) + + response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + +} + +fun getProxy(context: Context) = + getProxyInfo(context).proxy() + +fun getProxyInfo(context: Context) = + PreferenceManager.getDefaultSharedPreferences(context).getString("proxy", null).let { + if (it == null) + ProxyInfo(Proxy.Type.DIRECT) + else + json.parse(ProxyInfo.serializer(), it) + } \ 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 1d0c9091..94ae9ed1 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/update.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/update.kt @@ -32,7 +32,10 @@ import androidx.preference.PreferenceManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.serialization.json.* +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.content import ru.noties.markwon.Markwon import xyz.quaver.pupil.BuildConfig import xyz.quaver.pupil.R @@ -43,7 +46,7 @@ import java.util.* fun getReleases(url: String) : JsonArray { return try { URL(url).readText().let { - Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it) + json.parse(JsonArray.serializer(), it) } } catch (e: Exception) { JsonArray(emptyList()) @@ -145,6 +148,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) { setContentTitle(context.getString(R.string.update_notification_description)) setSmallIcon(android.R.drawable.stat_sys_download) priority = NotificationCompat.PRIORITY_LOW + setOngoing(true) } CoroutineScope(Dispatchers.IO).launch io@{ @@ -160,6 +164,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) { setContentText(context.getString(R.string.update_failed)) setMessage(context.getString(R.string.update_failed_message)) setSmallIcon(android.R.drawable.stat_sys_download_done) + setOngoing(false) } notificationManager.cancel(UPDATE_NOTIFICATION_ID) @@ -179,6 +184,7 @@ fun checkUpdate(context: AppCompatActivity, force: Boolean = false) { setSmallIcon(android.R.drawable.stat_sys_download_done) setContentTitle(context.getString(R.string.update_download_completed)) setContentText(context.getString(R.string.update_download_completed_description)) + setOngoing(false) } notificationManager.cancel(UPDATE_NOTIFICATION_ID) diff --git a/app/src/main/res/layout/dialog_proxy.xml b/app/src/main/res/layout/dialog_proxy.xml new file mode 100644 index 00000000..5765d86e --- /dev/null +++ b/app/src/main/res/layout/dialog_proxy.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +