Update in-app update checking
parent
c158c4e18e
commit
089e3dc209
@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appUpdateModule
|
||||
get() = module {
|
||||
single { AppUpdateRepository(androidContext(), get()) }
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
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.utils.ext.asArrayList
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
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"
|
||||
|
||||
class AppUpdateRepository(
|
||||
private val context: Context,
|
||||
private val okHttp: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val availableUpdate = MutableStateFlow<AppVersion?>(null)
|
||||
|
||||
fun observeAvailableUpdate() = availableUpdate.asStateFlow()
|
||||
|
||||
suspend fun getAvailableVersions(): List<AppVersion> {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/KotatsuApp/Kotatsu/releases?page=1&per_page=10")
|
||||
val jsonArray = okHttp.newCall(request.build()).await().parseJsonArray()
|
||||
return jsonArray.mapJSONNotNull { json ->
|
||||
val asset = json.optJSONArray("assets")?.optJSONObject(0) ?: return@mapJSONNotNull null
|
||||
AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url"),
|
||||
description = json.getString("body"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchUpdate(): AppVersion? {
|
||||
if (!isUpdateSupported()) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
val currentVersion = VersionId(BuildConfig.VERSION_NAME)
|
||||
val available = getAvailableVersions().asArrayList()
|
||||
available.sortBy { it.versionId }
|
||||
if (currentVersion.isStable) {
|
||||
available.retainAll { it.versionId.isStable }
|
||||
}
|
||||
available.maxByOrNull { it.versionId }
|
||||
?.takeIf { it.versionId > currentVersion }
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.onSuccess {
|
||||
availableUpdate.value = it
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun isUpdateSupported(): Boolean {
|
||||
return BuildConfig.DEBUG || 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()
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
val githubModule
|
||||
get() = module {
|
||||
factory { GithubRepository(get()) }
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||
|
||||
class GithubRepository(private val okHttp: OkHttpClient) {
|
||||
|
||||
suspend fun getLatestVersion(): AppVersion {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/KotatsuApp/Kotatsu/releases/latest")
|
||||
val json = okHttp.newCall(request.build()).await().parseJson()
|
||||
val asset = json.getJSONArray("assets").getJSONObject(0)
|
||||
return AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url"),
|
||||
description = json.getString("body")
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,49 +1,54 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import android.util.SparseIntArray
|
||||
import androidx.core.util.set
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.prefs.AppSection
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
|
||||
class MainViewModel(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val appUpdateRepository: AppUpdateRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
var defaultSection: AppSection
|
||||
get() = settings.defaultSection
|
||||
set(value) {
|
||||
settings.defaultSection = value
|
||||
}
|
||||
|
||||
val isSuggestionsEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
key = AppSettings.KEY_SUGGESTIONS,
|
||||
valueProducer = { isSuggestionsEnabled },
|
||||
)
|
||||
|
||||
val isTrackerEnabled = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
key = AppSettings.KEY_TRACKER_ENABLED,
|
||||
valueProducer = { isTrackerEnabled },
|
||||
)
|
||||
|
||||
val isResumeEnabled = historyRepository
|
||||
.observeHasItems()
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val counters = combine(
|
||||
appUpdateRepository.observeAvailableUpdate(),
|
||||
trackingRepository.observeUpdatedMangaCount(),
|
||||
) { appUpdate, tracks ->
|
||||
val a = SparseIntArray(2)
|
||||
a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
|
||||
a[R.id.nav_feed] = tracks
|
||||
a
|
||||
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
appUpdateRepository.fetchUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun openLastReader() {
|
||||
launchLoadingJob {
|
||||
val manga = historyRepository.getLastOrNull() ?: throw EmptyHistoryException()
|
||||
onOpenReader.call(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue