Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d81c22b586 |
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,6 +41,10 @@ class ParcelableManga(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableManga(manga=$manga, withChapters=$withChapters)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableManga {
|
||||
return ParcelableManga(parcel)
|
||||
|
||||
@@ -23,6 +23,10 @@ class ParcelableMangaChapters(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaChapters(chapters=$chapters)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
|
||||
return ParcelableMangaChapters(parcel)
|
||||
@@ -32,4 +36,4 @@ class ParcelableMangaChapters(
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ class ParcelableMangaPages(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaPages(pages=$pages)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
|
||||
return ParcelableMangaPages(parcel)
|
||||
@@ -32,4 +36,4 @@ class ParcelableMangaPages(
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ class ParcelableMangaTags(
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ParcelableMangaTags(tags=$tags)"
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
|
||||
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
|
||||
return ParcelableMangaTags(parcel)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<Any, String>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
owner.lifecycleScope.launch {
|
||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<List<SourceConfigItem>>(buildList())
|
||||
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) {
|
||||
if (isEnabled) {
|
||||
@@ -30,6 +38,7 @@ class NewSourcesViewModel @Inject constructor(
|
||||
settings.markKnownSources(initialList)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildList(): List<SourceConfigItem.SourceItem> {
|
||||
val locales = LocaleListCompat.getDefault().mapToSet { it.language }
|
||||
val pendingHidden = HashSet<String>()
|
||||
|
||||
Reference in New Issue
Block a user