Compare commits

...

1 Commits
v5.2 ... v5.2.1

Author SHA1 Message Date
Koitharu
d81c22b586 Fix crashes 2023-06-14 10:49:33 +03:00
14 changed files with 115 additions and 42 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 552 versionCode 553
versionName '5.2' versionName '5.2.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -12,6 +12,7 @@ import androidx.work.Configuration
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.acra.ACRA
import org.acra.ReportField import org.acra.ReportField
import org.acra.config.dialog import org.acra.config.dialog
import org.acra.config.httpSender import org.acra.config.httpSender
@@ -19,6 +20,7 @@ import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.koitharu.kotatsu.core.db.MangaDatabase 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.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
@@ -46,8 +48,12 @@ class KotatsuApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var workerFactory: HiltWorkerFactory lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var appValidator: AppValidator
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
enableStrictMode() enableStrictMode()
} }
@@ -90,6 +96,7 @@ class KotatsuApp : Application(), Configuration.Provider {
ReportField.CUSTOM_DATA, ReportField.CUSTOM_DATA,
ReportField.SHARED_PREFERENCES, ReportField.SHARED_PREFERENCES,
) )
dialog { dialog {
text = getString(R.string.crash_text) text = getString(R.string.crash_text)
title = getString(R.string.error_occurred) title = getString(R.string.error_occurred)

View File

@@ -1,9 +1,5 @@
package org.koitharu.kotatsu.core.github 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.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -14,28 +10,22 @@ import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.BaseHttpClient 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.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.await 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.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable 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.Inject
import javax.inject.Singleton 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" private const val CONTENT_TYPE_APK = "application/vnd.android.package-archive"
@Singleton @Singleton
class AppUpdateRepository @Inject constructor( class AppUpdateRepository @Inject constructor(
@ApplicationContext private val context: Context, private val appValidator: AppValidator,
private val settings: AppSettings, private val settings: AppSettings,
@BaseHttpClient private val okHttp: OkHttpClient, @BaseHttpClient private val okHttp: OkHttpClient,
) { ) {
@@ -85,7 +75,7 @@ class AppUpdateRepository @Inject constructor(
} }
fun isUpdateSupported(): Boolean { fun isUpdateSupported(): Boolean {
return BuildConfig.DEBUG || getCertificateSHA1Fingerprint() == CERT_SHA1 return BuildConfig.DEBUG || appValidator.isOriginalApp
} }
suspend fun getCurrentVersionChangelog(): String? { suspend fun getCurrentVersionChangelog(): String? {
@@ -94,22 +84,6 @@ class AppUpdateRepository @Inject constructor(
return available.find { x -> x.versionId == currentVersion }?.description 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? { private inline fun JSONArray.find(predicate: (JSONObject) -> Boolean): JSONObject? {
val size = length() val size = length()
for (i in 0 until size) { for (i in 0 until size) {

View File

@@ -41,6 +41,10 @@ class ParcelableManga(
return 0 return 0
} }
override fun toString(): String {
return "ParcelableManga(manga=$manga, withChapters=$withChapters)"
}
companion object CREATOR : Parcelable.Creator<ParcelableManga> { companion object CREATOR : Parcelable.Creator<ParcelableManga> {
override fun createFromParcel(parcel: Parcel): ParcelableManga { override fun createFromParcel(parcel: Parcel): ParcelableManga {
return ParcelableManga(parcel) return ParcelableManga(parcel)

View File

@@ -23,6 +23,10 @@ class ParcelableMangaChapters(
return 0 return 0
} }
override fun toString(): String {
return "ParcelableMangaChapters(chapters=$chapters)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> { companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters { override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
return ParcelableMangaChapters(parcel) return ParcelableMangaChapters(parcel)
@@ -32,4 +36,4 @@ class ParcelableMangaChapters(
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }
} }

View File

@@ -23,6 +23,10 @@ class ParcelableMangaPages(
return 0 return 0
} }
override fun toString(): String {
return "ParcelableMangaPages(pages=$pages)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> { companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages { override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
return ParcelableMangaPages(parcel) return ParcelableMangaPages(parcel)
@@ -32,4 +36,4 @@ class ParcelableMangaPages(
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }
} }

View File

@@ -24,6 +24,10 @@ class ParcelableMangaTags(
return 0 return 0
} }
override fun toString(): String {
return "ParcelableMangaTags(tags=$tags)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> { companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags { override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
return ParcelableMangaTags(parcel) return ParcelableMangaTags(parcel)

View File

@@ -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"
}
}

View File

@@ -13,6 +13,7 @@ import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.WeakHashMap
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -20,31 +21,40 @@ import javax.inject.Singleton
class AcraScreenLogger @Inject constructor() : FragmentLifecycleCallbacks(), DefaultActivityLifecycleCallbacks { class AcraScreenLogger @Inject constructor() : FragmentLifecycleCallbacks(), DefaultActivityLifecycleCallbacks {
private val timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.ROOT) private val timeFormat = SimpleDateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.ROOT)
private val keys = WeakHashMap<Any, String>()
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, 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) { override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
super.onFragmentDetached(fm, f) super.onFragmentDetached(fm, f)
ACRA.errorReporter.removeCustomData(f.key()) ACRA.errorReporter.removeCustomData(f.key())
keys.remove(f)
} }
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
super.onActivityCreated(activity, savedInstanceState) 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) (activity as? FragmentActivity)?.supportFragmentManager?.registerFragmentLifecycleCallbacks(this, true)
} }
override fun onActivityDestroyed(activity: Activity) { override fun onActivityDestroyed(activity: Activity) {
super.onActivityDestroyed(activity) super.onActivityDestroyed(activity)
ACRA.errorReporter.removeCustomData(activity.key()) ACRA.errorReporter.removeCustomData(activity.key())
keys.remove(activity)
} }
private fun Activity.key() = "Activity[${javaClass.simpleName}]" private fun Any.key() = keys.getOrPut(this) {
"${time()}: ${javaClass.simpleName}"
private fun Fragment.key() = "Fragment[${javaClass.simpleName}]" }
private fun time() = timeFormat.format(Date()) private fun time() = timeFormat.format(Date())
@Suppress("DEPRECATION")
private fun Bundle?.contentToString() = this?.keySet()?.joinToString { k ->
val v = get(k)
"$k=$v"
} ?: toString()
} }

View File

@@ -18,6 +18,14 @@ fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
} }
} }
fun <T> Flow<T>.observe(owner: LifecycleOwner, minState: Lifecycle.State, collector: FlowCollector<T>) {
owner.lifecycleScope.launch {
owner.lifecycle.repeatOnLifecycle(minState) {
collect(collector)
}
}
}
fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) { fun <T> Flow<Event<T>?>.observeEvent(owner: LifecycleOwner, collector: FlowCollector<T>) {
owner.lifecycleScope.launch { owner.lifecycleScope.launch {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {

View File

@@ -24,6 +24,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint 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.onPageSaved.observeEvent(this, this::onPageSaved)
viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged) viewModel.uiState.zipWithPrevious().observe(this, this::onUiStateChanged)
viewModel.isLoading.observe(this, this::onLoadingStateChanged) viewModel.isLoading.observe(this, this::onLoadingStateChanged)

View File

@@ -83,10 +83,10 @@ class MangaListActivity :
RemoteListFragment.newInstance(source) RemoteListFragment.newInstance(source)
} }
replace(R.id.container, fragment) replace(R.id.container, fragment)
runOnCommit { initFilter() }
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) {
runOnCommit(ApplyFilterRunnable(fragment, tags)) runOnCommit(ApplyFilterRunnable(fragment, tags))
} }
runOnCommit { initFilter() }
} }
} else { } else {
initFilter() initFilter()

View File

@@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
@@ -38,7 +39,8 @@ class NewSourcesDialogFragment :
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.new_sources_text) 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 { override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.settings.newsources package org.koitharu.kotatsu.settings.newsources
import androidx.annotation.WorkerThread
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.getLocaleTitle import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -15,8 +17,14 @@ class NewSourcesViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
val sources = MutableStateFlow<List<SourceConfigItem>>(buildList())
private val initialList = settings.newSources private val initialList = settings.newSources
val sources = MutableStateFlow<List<SourceConfigItem>?>(null)
init {
launchJob(Dispatchers.Default) {
sources.value = buildList()
}
}
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
if (isEnabled) { if (isEnabled) {
@@ -30,6 +38,7 @@ class NewSourcesViewModel @Inject constructor(
settings.markKnownSources(initialList) settings.markKnownSources(initialList)
} }
@WorkerThread
private fun buildList(): List<SourceConfigItem.SourceItem> { private fun buildList(): List<SourceConfigItem.SourceItem> {
val locales = LocaleListCompat.getDefault().mapToSet { it.language } val locales = LocaleListCompat.getDefault().mapToSet { it.language }
val pendingHidden = HashSet<String>() val pendingHidden = HashSet<String>()