Update in-app update checking

This commit is contained in:
Koitharu
2022-07-21 19:32:32 +03:00
parent c158c4e18e
commit 089e3dc209
19 changed files with 232 additions and 105 deletions

View File

@@ -18,7 +18,7 @@ import org.koin.core.context.startKoin
import org.koitharu.kotatsu.bookmarks.bookmarksModule
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.github.appUpdateModule
import org.koitharu.kotatsu.core.network.networkModule
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.uiModule
@@ -61,7 +61,7 @@ class KotatsuApp : Application() {
modules(
networkModule,
databaseModule,
githubModule,
appUpdateModule,
uiModule,
mainModule,
searchModule,
@@ -136,7 +136,7 @@ class KotatsuApp : Application() {
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
@@ -145,7 +145,7 @@ class KotatsuApp : Application() {
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()

View File

@@ -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()) }
}

View File

@@ -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()
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.github
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -10,5 +11,9 @@ data class AppVersion(
val url: String,
val apkSize: Long,
val apkUrl: String,
val description: String
) : Parcelable
val description: String,
) : Parcelable {
@IgnoredOnParcel
val versionId = VersionId(name)
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.core.github
import org.koin.dsl.module
val githubModule
get() = module {
factory { GithubRepository(get()) }
}

View File

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

View File

@@ -63,6 +63,9 @@ class VersionId(
}
}
val VersionId.isStable: Boolean
get() = variantType.isEmpty()
fun VersionId(versionName: String): VersionId {
val parts = versionName.substringBeforeLast('-').split('.')
val variant = versionName.substringAfterLast('-', "")
@@ -73,4 +76,4 @@ fun VersionId(versionName: String): VersionId {
variantType = variant.filter(Char::isLetter),
variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0,
)
}
}

View File

@@ -24,6 +24,6 @@ val mainModule
factory { ShortcutsUpdater(androidContext(), get(), get(), get()) }
}
viewModel { MainViewModel(get(), get()) }
viewModel { MainViewModel(get(), get(), get(), get()) }
viewModel { ProtectViewModel(get(), get()) }
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.main.ui
import android.os.Bundle
import android.util.SparseIntArray
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@@ -9,12 +10,14 @@ import androidx.annotation.IdRes
import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.Insets
import androidx.core.util.size
import androidx.core.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionManager
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.navigation.NavigationBarView
@@ -40,7 +43,6 @@ import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment
@@ -50,7 +52,6 @@ import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.*
import com.google.android.material.R as materialR
private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search"
@@ -60,7 +61,8 @@ class MainActivity :
AppBarOwner,
View.OnClickListener,
View.OnFocusChangeListener,
SearchSuggestionListener, NavigationBarView.OnItemSelectedListener {
SearchSuggestionListener,
NavigationBarView.OnItemSelectedListener {
private val viewModel by viewModel<MainViewModel>()
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()
@@ -108,6 +110,7 @@ class MainActivity :
viewModel.onError.observe(this, this::onError)
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -269,6 +272,20 @@ class MainActivity :
Snackbar.make(binding.container, e.getDisplayMessage(resources), Snackbar.LENGTH_SHORT).show()
}
private fun onCountersChanged(counters: SparseIntArray) {
repeat(counters.size) { i ->
val id = counters.keyAt(i)
val counter = counters.valueAt(i)
if (counter == 0) {
navBar.getBadge(id)?.isVisible = false
} else {
val badge = navBar.getOrCreateBadge(id)
badge.number = counter
badge.isVisible = true
}
}
}
private fun onLoadingStateChanged(isLoading: Boolean) {
binding.fab?.isEnabled = !isLoading
}
@@ -330,13 +347,9 @@ class MainActivity :
private fun onFirstStart() {
lifecycleScope.launchWhenResumed {
val isUpdateSupported = withContext(Dispatchers.Default) {
withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
AppUpdateChecker.isUpdateSupported(this@MainActivity)
}
if (isUpdateSupported) {
AppUpdateChecker(this@MainActivity).checkIfNeeded()
}
val settings = get<AppSettings>()
when {
@@ -378,4 +391,4 @@ class MainActivity :
}
}
}
}
}

View File

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

View File

@@ -7,31 +7,32 @@ import android.content.pm.PackageManager
import androidx.activity.ComponentActivity
import androidx.annotation.MainThread
import androidx.core.net.toUri
import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
@Deprecated("")
class AppUpdateChecker(private val activity: ComponentActivity) {
private val settings = activity.get<AppSettings>()
private val repo = activity.get<GithubRepository>()
private val repo = activity.get<AppUpdateRepository>()
suspend fun checkIfNeeded(): Boolean? = if (
settings.isUpdateCheckingEnabled &&
@@ -43,7 +44,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
}
suspend fun checkNow() = runCatching {
val version = repo.getLatestVersion()
val version = repo.fetchUpdate() ?: return@runCatching false
val newVersionId = VersionId(version.name)
val currentVersionId = VersionId(BuildConfig.VERSION_NAME)
val result = newVersionId > currentVersionId
@@ -107,4 +108,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
error.printStackTraceDebug()
}.getOrNull()
}
}
}

View File

@@ -35,5 +35,5 @@ val settingsModule
viewModel { OnboardViewModel(get()) }
viewModel { SourcesSettingsViewModel(get()) }
viewModel { NewSourcesViewModel(get()) }
viewModel { ToolsViewModel(get(), get()) }
viewModel { ToolsViewModel(get(), get(), get()) }
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.tools
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.transition.TransitionManager
@@ -10,6 +11,7 @@ import android.widget.CompoundButton
import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.core.widget.TextViewCompat
@@ -19,9 +21,9 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.databinding.FragmentToolsBinding
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.settings.AppUpdateChecker
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
import org.koitharu.kotatsu.utils.FileSize
@@ -32,7 +34,6 @@ class ToolsFragment :
CompoundButton.OnCheckedChangeListener,
View.OnClickListener {
private var updateChecker: AppUpdateChecker? = null
private val viewModel by viewModel<ToolsViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding {
@@ -51,12 +52,19 @@ class ToolsFragment :
binding.switchIncognito.isChecked = it
}
viewModel.storageUsage.observe(viewLifecycleOwner, ::onStorageUsageChanged)
viewModel.appUpdate.observe(viewLifecycleOwner, ::onAppUpdateAvailable)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_settings -> startActivity(SettingsActivity.newIntent(v.context))
R.id.button_downloads -> startActivity(DownloadsActivity.newIntent(v.context))
R.id.button_download -> {
val url = viewModel.appUpdate.value?.apkUrl ?: return
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url.toUri()
startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser)))
}
}
}
@@ -72,6 +80,15 @@ class ToolsFragment :
)
}
private fun onAppUpdateAvailable(version: AppVersion?) {
if (version == null) {
binding.cardUpdate.root.isVisible = false
return
}
binding.cardUpdate.textPrimary.text = getString(R.string.new_version_s, version.name)
binding.cardUpdate.root.isVisible = true
}
private fun onStorageUsageChanged(usage: StorageUsage) {
val storageSegment = SegmentedBarView.Segment(usage.savedManga.percent, segmentColor(1))
val pagesSegment = SegmentedBarView.Segment(usage.pagesCache.percent, segmentColor(2))

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.settings.tools
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.BaseViewModel
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.local.data.CacheDir
@@ -13,9 +15,13 @@ import org.koitharu.kotatsu.settings.tools.model.StorageUsage
class ToolsViewModel(
private val storageManager: LocalStorageManager,
private val appUpdateRepository: AppUpdateRepository,
private val settings: AppSettings,
) : BaseViewModel() {
val appUpdate = appUpdateRepository.observeAvailableUpdate()
.asLiveData(viewModelScope.coroutineContext)
val storageUsage: LiveData<StorageUsage> = liveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
) {

View File

@@ -18,6 +18,9 @@ abstract class TracksDao {
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun findNewChapters(mangaId: Long): Int?
@Query("SELECT chapters_new FROM tracks")
abstract fun observeNewChapters(): Flow<List<Int>>
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
abstract fun observeNewChapters(mangaId: Long): Flow<Int?>
@@ -42,4 +45,4 @@ abstract class TracksDao {
insert(entity)
}
}
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.tracker.domain
import androidx.annotation.VisibleForTesting
import androidx.room.withTransaction
import java.util.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -18,7 +19,6 @@ import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import java.util.*
private const val NO_ID = 0L
@@ -34,6 +34,10 @@ class TrackingRepository(
return db.tracksDao.observeNewChapters(mangaId).map { it ?: 0 }
}
fun observeUpdatedMangaCount(): Flow<Int> {
return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } }
}
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
val ids = mangaList.mapToSet { it.id }
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId }
@@ -47,7 +51,7 @@ class TrackingRepository(
result += MangaTracking(
manga = item,
lastChapterId = track?.lastChapterId ?: NO_ID,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date),
)
}
return result
@@ -59,7 +63,7 @@ class TrackingRepository(
return MangaTracking(
manga = manga,
lastChapterId = track?.lastChapterId ?: NO_ID,
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date)
lastCheck = track?.lastCheck?.takeUnless { it == 0L }?.let(::Date),
)
}
@@ -166,4 +170,4 @@ class TrackingRepository(
}
private fun Collection<MangaEntity>.toMangaList() = map { it.toManga(emptySet()) }
}
}

View File

@@ -3,11 +3,11 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.utils.BufferedObserver
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun <T> LiveData<T>.requireValue(): T = checkNotNull(value) {
"LiveData value is null"
@@ -22,12 +22,12 @@ fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: Buffere
}
fun <T> StateFlow<T>.asLiveDataDistinct(
context: CoroutineContext = EmptyCoroutineContext
context: CoroutineContext = EmptyCoroutineContext,
): LiveData<T> = asLiveDataDistinct(context, value)
fun <T> Flow<T>.asLiveDataDistinct(
context: CoroutineContext = EmptyCoroutineContext,
defaultValue: T
defaultValue: T,
): LiveData<T> = liveData(context) {
if (latestValue == null) {
emit(defaultValue)
@@ -37,4 +37,4 @@ fun <T> Flow<T>.asLiveDataDistinct(
emit(it)
}
}
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -14,7 +15,9 @@
layout="@layout/layout_app_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal" />
android:layout_margin="@dimen/margin_normal"
android:visibility="gone"
tools:visibility="visible" />
<include
android:id="@+id/layout_sync"

View File

@@ -9,10 +9,10 @@ import org.junit.Test
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.parsers.util.await
class GithubRepositoryTest {
class AppUpdateRepositoryTest {
private val okHttpClient = OkHttpClient()
private val repository = GithubRepository(okHttpClient)
private val repository = AppUpdateRepository(okHttpClient)
@Test
fun getLatestVersion() = runTest {
@@ -23,11 +23,11 @@ class GithubRepositoryTest {
Request.Builder()
.url(version.apkUrl)
.head()
.build()
.build(),
).await()
Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME))
Assert.assertTrue(apkHead.isSuccessful)
Assert.assertEquals(version.apkSize, apkHead.headersContentLength())
}
}
}