diff --git a/app/build.gradle b/app/build.gradle index 8e08ece56..499ad45b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 33 - versionCode 552 - versionName '5.2' + versionCode 553 + versionName '5.2.1' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index ee0de4d61..7a0e3747f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -12,6 +12,7 @@ import androidx.work.Configuration import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.acra.ACRA import org.acra.ReportField import org.acra.config.dialog import org.acra.config.httpSender @@ -19,6 +20,7 @@ import org.acra.data.StringFormat import org.acra.ktx.initAcra import org.acra.sender.HttpSender import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.os.AppValidator import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.WorkServiceStopHelper import org.koitharu.kotatsu.core.util.ext.processLifecycleScope @@ -46,8 +48,12 @@ class KotatsuApp : Application(), Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory + @Inject + lateinit var appValidator: AppValidator + override fun onCreate() { super.onCreate() + ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString()) if (BuildConfig.DEBUG) { enableStrictMode() } @@ -90,6 +96,7 @@ class KotatsuApp : Application(), Configuration.Provider { ReportField.CUSTOM_DATA, ReportField.SHARED_PREFERENCES, ) + dialog { text = getString(R.string.crash_text) title = getString(R.string.error_occurred) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt index 4d26db92a..a1d2a5544 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt @@ -1,9 +1,5 @@ package org.koitharu.kotatsu.core.github -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.PackageManager -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,28 +10,22 @@ import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.network.BaseHttpClient +import org.koitharu.kotatsu.core.os.AppValidator import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.util.await -import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import javax.inject.Inject import javax.inject.Singleton -private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" private const val CONTENT_TYPE_APK = "application/vnd.android.package-archive" @Singleton class AppUpdateRepository @Inject constructor( - @ApplicationContext private val context: Context, + private val appValidator: AppValidator, private val settings: AppSettings, @BaseHttpClient private val okHttp: OkHttpClient, ) { @@ -85,7 +75,7 @@ class AppUpdateRepository @Inject constructor( } fun isUpdateSupported(): Boolean { - return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1 + return BuildConfig.DEBUG || appValidator.isOriginalApp } suspend fun getCurrentVersionChangelog(): String? { @@ -94,22 +84,6 @@ class AppUpdateRepository @Inject constructor( return available.find { x -> x.versionId == currentVersion }?.description } - @Suppress("DEPRECATION") - @SuppressLint("PackageManagerGetSignatures") - private fun getCertificateSHA1Fingerprint(): String? = runCatching { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) - val signatures = requireNotNull(packageInfo?.signatures) - val cert: ByteArray = signatures.first().toByteArray() - val input: InputStream = ByteArrayInputStream(cert) - val cf = CertificateFactory.getInstance("X509") - val c = cf.generateCertificate(input) as X509Certificate - val md: MessageDigest = MessageDigest.getInstance("SHA1") - val publicKey: ByteArray = md.digest(c.encoded) - return publicKey.byte2HexFormatted() - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrNull() - private inline fun JSONArray.find(predicate: (JSONObject) -> Boolean): JSONObject? { val size = length() for (i in 0 until size) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt index ef778f173..f5b578700 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableManga.kt @@ -41,6 +41,10 @@ class ParcelableManga( return 0 } + override fun toString(): String { + return "ParcelableManga(manga=$manga, withChapters=$withChapters)" + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): ParcelableManga { return ParcelableManga(parcel) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt index 473b45320..6196069ba 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaChapters.kt @@ -23,6 +23,10 @@ class ParcelableMangaChapters( return 0 } + override fun toString(): String { + return "ParcelableMangaChapters(chapters=$chapters)" + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters { return ParcelableMangaChapters(parcel) @@ -32,4 +36,4 @@ class ParcelableMangaChapters( return arrayOfNulls(size) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt index 3230ec59b..593e01600 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaPages.kt @@ -23,6 +23,10 @@ class ParcelableMangaPages( return 0 } + override fun toString(): String { + return "ParcelableMangaPages(pages=$pages)" + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): ParcelableMangaPages { return ParcelableMangaPages(parcel) @@ -32,4 +36,4 @@ class ParcelableMangaPages( return arrayOfNulls(size) } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt index 7f6cf2f42..bcfca9864 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/parcelable/ParcelableMangaTags.kt @@ -24,6 +24,10 @@ class ParcelableMangaTags( return 0 } + override fun toString(): String { + return "ParcelableMangaTags(tags=$tags)" + } + companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): ParcelableMangaTags { return ParcelableMangaTags(parcel) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppValidator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppValidator.kt new file mode 100644 index 000000000..da18d57a4 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/AppValidator.kt @@ -0,0 +1,46 @@ +package org.koitharu.kotatsu.core.os + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import dagger.hilt.android.qualifiers.ApplicationContext +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.parsers.util.byte2HexFormatted +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AppValidator @Inject constructor( + @ApplicationContext private val context: Context, +) { + + val isOriginalApp by lazy { + getCertificateSHA1Fingerprint() == CERT_SHA1 + } + + @Suppress("DEPRECATION") + @SuppressLint("PackageManagerGetSignatures") + private fun getCertificateSHA1Fingerprint(): String? = runCatching { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) + val signatures = requireNotNull(packageInfo?.signatures) + val cert: ByteArray = signatures.first().toByteArray() + val input: InputStream = ByteArrayInputStream(cert) + val cf = CertificateFactory.getInstance("X509") + val c = cf.generateCertificate(input) as X509Certificate + val md: MessageDigest = MessageDigest.getInstance("SHA1") + val publicKey: ByteArray = md.digest(c.encoded) + return publicKey.byte2HexFormatted() + }.onFailure { error -> + error.printStackTraceDebug() + }.getOrNull() + + private companion object { + + private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AcraScreenLogger.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AcraScreenLogger.kt index 5d768b82f..104446a3f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AcraScreenLogger.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/AcraScreenLogger.kt @@ -13,6 +13,7 @@ import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import java.util.WeakHashMap import javax.inject.Inject import javax.inject.Singleton @@ -20,31 +21,40 @@ import javax.inject.Singleton class AcraScreenLogger @Inject constructor() : FragmentLifecycleCallbacks(), DefaultActivityLifecycleCallbacks { private val timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.ROOT) + private val keys = WeakHashMap() override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { super.onFragmentAttached(fm, f, context) - ACRA.errorReporter.putCustomData(f.key(), "${time()}: ${f.arguments}") + ACRA.errorReporter.putCustomData(f.key(), f.arguments.contentToString()) } override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { super.onFragmentDetached(fm, f) ACRA.errorReporter.removeCustomData(f.key()) + keys.remove(f) } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { super.onActivityCreated(activity, savedInstanceState) - ACRA.errorReporter.putCustomData(activity.key(), "${time()}: ${activity.intent}") + ACRA.errorReporter.putCustomData(activity.key(), activity.intent.extras.contentToString()) (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(this, true) } override fun onActivityDestroyed(activity: Activity) { super.onActivityDestroyed(activity) ACRA.errorReporter.removeCustomData(activity.key()) + keys.remove(activity) } - private fun Activity.key() = "Activity[${javaClass.simpleName}]" - - private fun Fragment.key() = "Fragment[${javaClass.simpleName}]" + private fun Any.key() = keys.getOrPut(this) { + "${time()}: ${javaClass.simpleName}" + } private fun time() = timeFormat.format(Date()) + + @Suppress("DEPRECATION") + private fun Bundle?.contentToString() = this?.keySet()?.joinToString { k -> + val v = get(k) + "$k=$v" + } ?: toString() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt index d6c634468..f5e5b68a1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/FlowObserver.kt @@ -18,6 +18,14 @@ fun Flow.observe(owner: LifecycleOwner, collector: FlowCollector) { } } +fun Flow.observe(owner: LifecycleOwner, minState: Lifecycle.State, collector: FlowCollector) { + owner.lifecycleScope.launch { + owner.lifecycle.repeatOnLifecycle(minState) { + collect(collector) + } + } +} + fun Flow?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector) { owner.lifecycleScope.launch { owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index c33f1b076..63702d529 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -24,6 +24,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -127,7 +128,7 @@ class ReaderActivity : }, ), ) - viewModel.readerMode.observe(this, this::onInitReader) + viewModel.readerMode.observe(this, Lifecycle.State.STARTED, this::onInitReader) viewModel.onPageSaved.observeEvent(this, this::onPageSaved) viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt index bec248475..d9ec0af60 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/MangaListActivity.kt @@ -83,10 +83,10 @@ class MangaListActivity : RemoteListFragment.newInstance(source) } replace(R.id.container, fragment) + runOnCommit { initFilter() } if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { runOnCommit(ApplyFilterRunnable(fragment, tags)) } - runOnCommit { initFilter() } } } else { initFilter() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt index bec4ad77f..b35024a20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesDialogFragment.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels import coil.ImageLoader import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.util.ext.observe @@ -38,7 +39,8 @@ class NewSourcesDialogFragment : binding.recyclerView.adapter = adapter binding.textViewTitle.setText(R.string.new_sources_text) - viewModel.sources.observe(viewLifecycleOwner) { adapter.items = it } + viewModel.sources.filterNotNull() + .observe(viewLifecycleOwner) { adapter.items = it } } override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt index 1a2a935a9..1c00899b3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/newsources/NewSourcesViewModel.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.settings.newsources +import androidx.annotation.WorkerThread import androidx.core.os.LocaleListCompat import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.prefs.AppSettings @@ -15,8 +17,14 @@ class NewSourcesViewModel @Inject constructor( private val settings: AppSettings, ) : BaseViewModel() { - val sources = MutableStateFlow>(buildList()) private val initialList = settings.newSources + val sources = MutableStateFlow?>(null) + + init { + launchJob(Dispatchers.Default) { + sources.value = buildList() + } + } fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { if (isEnabled) { @@ -30,6 +38,7 @@ class NewSourcesViewModel @Inject constructor( settings.markKnownSources(initialList) } + @WorkerThread private fun buildList(): List { val locales = LocaleListCompat.getDefault().mapToSet { it.language } val pendingHidden = HashSet()