Migrate app update logic from RxJava to Coroutines

This commit is contained in:
Rafael Caetano 2023-11-05 16:31:57 +00:00
parent c03c02f0af
commit 4b67d6c3be
13 changed files with 222 additions and 217 deletions

View File

@ -1,13 +1,12 @@
package me.magnum.melonds.github
import io.reactivex.Single
import me.magnum.melonds.github.dtos.ReleaseDto
import retrofit2.http.GET
interface GitHubApi {
@GET("/repos/rafaelvcaetano/melonDS-android/releases/latest")
fun getLatestRelease(): Single<ReleaseDto>
suspend fun getLatestRelease(): ReleaseDto
@GET("/repos/rafaelvcaetano/melonDS-android/releases/tags/nightly-release")
fun getLatestNightlyRelease(): Single<ReleaseDto>
suspend fun getLatestNightlyRelease(): ReleaseDto
}

View File

@ -8,10 +8,13 @@ import android.content.IntentFilter
import android.database.ContentObserver
import android.net.Uri
import androidx.core.content.getSystemService
import io.reactivex.Observable
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import me.magnum.melonds.common.providers.UpdateContentProvider
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.services.UpdateInstallManager
import java.io.File
@ -32,77 +35,77 @@ class GitHubUpdateInstallManager(private val context: Context) : UpdateInstallMa
context.registerReceiver(downloadCompleteReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
override fun downloadAndInstallUpdate(update: AppUpdate): Observable<DownloadProgress> {
return Observable.create { emitter ->
val updatesFolder = context.externalCacheDir?.let { File(it, "updates") }
if (updatesFolder == null) {
emitter.onComplete()
return@create
}
override fun downloadAndInstallUpdate(update: AppUpdate): Flow<DownloadProgress> = flow {
val updatesFolder = context.externalCacheDir?.let { File(it, "updates") }
if (updatesFolder == null) {
return@flow
}
if (!updatesFolder.isDirectory && !updatesFolder.mkdirs()) {
emitter.onComplete()
return@create
}
if (!updatesFolder.isDirectory && !updatesFolder.mkdirs()) {
return@flow
}
val destinationFile = File(updatesFolder, "update.apk")
if (destinationFile.isFile) {
destinationFile.delete()
}
val destinationFile = File(updatesFolder, "update.apk")
if (destinationFile.isFile) {
destinationFile.delete()
}
val destinationUri = UpdateContentProvider.getUpdateFileUri(context, destinationFile)
val downloadManager = context.getSystemService<DownloadManager>()!!
val request = DownloadManager.Request(update.downloadUri).apply {
setDestinationUri(destinationUri)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setMimeType("application/vnd.android.package-archive")
setTitle("Downloading update ${update.newVersion}...")
}
val downloadId = downloadManager.enqueue(request)
pendingDownloadId = downloadId
val destinationUri = UpdateContentProvider.getUpdateFileUri(context, destinationFile)
val downloadManager = context.getSystemService<DownloadManager>()!!
val request = DownloadManager.Request(update.downloadUri).apply {
setDestinationUri(destinationUri)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setMimeType("application/vnd.android.package-archive")
setTitle("Downloading update ${update.newVersion}...")
}
val downloadId = downloadManager.enqueue(request)
pendingDownloadId = downloadId
val downloadUri = Uri.parse("content://downloads/my_downloads/${downloadId}")
context.contentResolver.registerContentObserver(downloadUri, false, object : ContentObserver(null) {
init {
emitter.setCancellable {
context.contentResolver.unregisterContentObserver(this)
}
startDownload(downloadManager, downloadId).collect(this)
}
private suspend fun startDownload(downloadManager: DownloadManager, downloadId: Long): Flow<DownloadProgress> = callbackFlow {
val downloadContentObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
val query = DownloadManager.Query().apply {
setFilterById(downloadId)
}
override fun onChange(selfChange: Boolean, uri: Uri?) {
val query = DownloadManager.Query().apply {
setFilterById(downloadId)
val cursor = downloadManager.query(query)
if (cursor.moveToNext()) {
val sizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val downloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val size = cursor.getLong(sizeIndex)
val downloaded = cursor.getLong(downloadedIndex)
val status = cursor.getInt(statusIndex)
val isFinished = status == DownloadManager.STATUS_FAILED || status == DownloadManager.STATUS_SUCCESSFUL
if (size >= 0) {
channel.trySend(DownloadProgress.DownloadUpdate(size, downloaded))
}
val cursor = downloadManager.query(query)
if (cursor.moveToNext()) {
val sizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val downloadedIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val size = cursor.getLong(sizeIndex)
val downloaded = cursor.getLong(downloadedIndex)
val status = cursor.getInt(statusIndex)
val isFinished = status == DownloadManager.STATUS_FAILED || status == DownloadManager.STATUS_SUCCESSFUL
if (size >= 0) {
emitter.onNext(DownloadProgress.DownloadUpdate(size, downloaded))
if (isFinished) {
if (status == DownloadManager.STATUS_SUCCESSFUL) {
channel.trySend(DownloadProgress.DownloadComplete)
} else {
channel.trySend(DownloadProgress.DownloadFailed)
}
if (isFinished) {
if (status == DownloadManager.STATUS_SUCCESSFUL) {
emitter.onNext(DownloadProgress.DownloadComplete)
} else {
emitter.onNext(DownloadProgress.DownloadFailed)
}
emitter.onComplete()
context.contentResolver.unregisterContentObserver(this)
}
channel.close()
}
}
})
}
}
val downloadUri = Uri.parse("content://downloads/my_downloads/${downloadId}")
context.contentResolver.registerContentObserver(downloadUri, false, downloadContentObserver)
awaitClose {
context.contentResolver.unregisterContentObserver(downloadContentObserver)
}
}

View File

@ -3,8 +3,8 @@ package me.magnum.melonds.github.repositories
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.core.net.toUri
import io.reactivex.Maybe
import io.reactivex.Single
import me.magnum.melonds.common.suspendMapCatching
import me.magnum.melonds.common.suspendRunCatching
import me.magnum.melonds.domain.model.Version
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.repositories.UpdatesRepository
@ -21,35 +21,33 @@ class GitHubNightlyUpdatesRepository(private val api: GitHubApi, private val pre
private const val KEY_LAST_RELEASE_DATE = "github_updates_nightly_last_release_date"
}
override fun checkNewUpdate(): Maybe<AppUpdate> {
return shouldCheckUpdates()
.flatMapMaybe { checkUpdates ->
if (checkUpdates) {
api.getLatestNightlyRelease().flatMapMaybe { release ->
if (shouldUpdate(release)) {
val apkBinary = release.assets.firstOrNull { it.contentType == APK_CONTENT_TYPE }
if (apkBinary != null) {
val update = AppUpdate(
AppUpdate.Type.NIGHTLY,
apkBinary.id,
apkBinary.url.toUri(),
Version.fromString(release.tagName),
release.body,
apkBinary.size,
Instant.parse(release.createdAt),
)
Maybe.just(update)
} else {
Maybe.empty()
}
} else {
Maybe.empty()
}
}
override suspend fun checkNewUpdate(): Result<AppUpdate?> {
if (!shouldCheckUpdates()) {
return Result.success(null)
}
return suspendRunCatching {
api.getLatestNightlyRelease()
}.suspendMapCatching { release ->
if (shouldUpdate(release)) {
val apkBinary = release.assets.firstOrNull { it.contentType == APK_CONTENT_TYPE }
if (apkBinary != null) {
AppUpdate(
AppUpdate.Type.NIGHTLY,
apkBinary.id,
apkBinary.url.toUri(),
Version.fromString(release.tagName),
release.body,
apkBinary.size,
Instant.parse(release.createdAt),
)
} else {
Maybe.empty()
null
}
} else {
null
}
}
}
override fun skipUpdate(update: AppUpdate) {
@ -64,25 +62,19 @@ class GitHubNightlyUpdatesRepository(private val api: GitHubApi, private val pre
}
}
private fun shouldCheckUpdates(): Single<Boolean> {
return Single.create { emitter ->
val updateCheckEnabled = preferences.getBoolean(PREF_KEY_GITHUB_CHECK_FOR_UPDATES, true)
if (!updateCheckEnabled) {
emitter.onSuccess(false)
return@create
}
val nextUpdateCheckTime = preferences.getLong(KEY_NEXT_CHECK_DATE, -1)
if (nextUpdateCheckTime == (-1).toLong()) {
emitter.onSuccess(true)
return@create
}
val now = Instant.now()
val shouldCheckUpdates = now.toEpochMilli() > nextUpdateCheckTime
emitter.onSuccess(shouldCheckUpdates)
private fun shouldCheckUpdates(): Boolean {
val updateCheckEnabled = preferences.getBoolean(PREF_KEY_GITHUB_CHECK_FOR_UPDATES, true)
if (!updateCheckEnabled) {
return false
}
val nextUpdateCheckTime = preferences.getLong(KEY_NEXT_CHECK_DATE, -1)
if (nextUpdateCheckTime == (-1).toLong()) {
return true
}
val now = Instant.now()
return now.toEpochMilli() > nextUpdateCheckTime
}
private fun scheduleNextUpdate() {

View File

@ -4,10 +4,10 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.core.net.toUri
import io.reactivex.Maybe
import io.reactivex.Single
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.common.suspendMapCatching
import me.magnum.melonds.common.suspendRunCatching
import me.magnum.melonds.domain.model.Version
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.repositories.UpdatesRepository
import me.magnum.melonds.github.GitHubApi
import me.magnum.melonds.github.PREF_KEY_GITHUB_CHECK_FOR_UPDATES
@ -15,7 +15,7 @@ import me.magnum.melonds.github.dtos.ReleaseDto
import me.magnum.melonds.utils.PackageManagerCompat
import me.magnum.melonds.utils.enumValueOfIgnoreCase
import java.time.Instant
import java.util.*
import java.util.Calendar
import java.util.concurrent.TimeUnit
class GitHubProdUpdatesRepository(private val context: Context, private val api: GitHubApi, private val preferences: SharedPreferences) : UpdatesRepository {
@ -27,36 +27,35 @@ class GitHubProdUpdatesRepository(private val context: Context, private val api:
private const val UPDATE_CHECK_DELAY_HOURS = 22
}
override fun checkNewUpdate(): Maybe<AppUpdate> {
return shouldCheckUpdates()
.flatMapMaybe { checkUpdates ->
if (checkUpdates) {
api.getLatestRelease().flatMapMaybe { release ->
updateLastUpdateCheckTime()
if (isReleaseNewUpdate(release) && !shouldSkipUpdate(release)) {
val apkBinary = release.assets.firstOrNull { it.contentType == APK_CONTENT_TYPE }
if (apkBinary != null) {
val update = AppUpdate(
AppUpdate.Type.PRODUCTION,
apkBinary.id,
apkBinary.url.toUri(),
Version.fromString(release.tagName),
release.body,
apkBinary.size,
Instant.parse(release.createdAt),
)
Maybe.just(update)
} else {
Maybe.empty()
}
} else {
Maybe.empty()
}
}
override suspend fun checkNewUpdate(): Result<AppUpdate?> {
if (!shouldCheckUpdates()) {
return Result.success(null)
}
return suspendRunCatching {
api.getLatestRelease()
}.suspendMapCatching { release ->
updateLastUpdateCheckTime()
if (isReleaseNewUpdate(release) && !shouldSkipUpdate(release)) {
val apkBinary = release.assets.firstOrNull { it.contentType == APK_CONTENT_TYPE }
if (apkBinary != null) {
AppUpdate(
AppUpdate.Type.PRODUCTION,
apkBinary.id,
apkBinary.url.toUri(),
Version.fromString(release.tagName),
release.body,
apkBinary.size,
Instant.parse(release.createdAt),
)
} else {
Maybe.empty()
null
}
} else {
null
}
}
}
override fun skipUpdate(update: AppUpdate) {
@ -69,28 +68,23 @@ class GitHubProdUpdatesRepository(private val context: Context, private val api:
// Do nothing
}
private fun shouldCheckUpdates(): Single<Boolean> {
return Single.create { emitter ->
val updateCheckEnabled = preferences.getBoolean(PREF_KEY_GITHUB_CHECK_FOR_UPDATES, true)
if (!updateCheckEnabled) {
emitter.onSuccess(false)
return@create
}
val lastCheckUpdateTimestamp = preferences.getLong(KEY_LAST_UPDATE_CHECK, -1)
if (lastCheckUpdateTimestamp == (-1).toLong()) {
emitter.onSuccess(true)
return@create
}
val currentDate = Calendar.getInstance().time
val difference = currentDate.time - lastCheckUpdateTimestamp
val hoursDifference = TimeUnit.HOURS.convert(difference, TimeUnit.MILLISECONDS)
val shouldCheckUpdates = hoursDifference >= UPDATE_CHECK_DELAY_HOURS
emitter.onSuccess(shouldCheckUpdates)
private fun shouldCheckUpdates(): Boolean {
val updateCheckEnabled = preferences.getBoolean(PREF_KEY_GITHUB_CHECK_FOR_UPDATES, true)
if (!updateCheckEnabled) {
return true
}
val lastCheckUpdateTimestamp = preferences.getLong(KEY_LAST_UPDATE_CHECK, -1)
if (lastCheckUpdateTimestamp == (-1).toLong()) {
return true
}
val currentDate = Calendar.getInstance().time
val difference = currentDate.time - lastCheckUpdateTimestamp
val hoursDifference = TimeUnit.HOURS.convert(difference, TimeUnit.MILLISECONDS)
return hoursDifference >= UPDATE_CHECK_DELAY_HOURS
}
private fun updateLastUpdateCheckTime() {

View File

@ -4,10 +4,14 @@ data class Version(val type: ReleaseType, val major: Int, val minor: Int, val pa
enum class ReleaseType {
ALPHA,
BETA,
FINAL
FINAL,
NIGHTLY,
}
companion object {
val Nightly = Version(ReleaseType.NIGHTLY, -1, -1, -1)
/**
* Converts a version string in the format of `[alpha|beta-]major.minor.patch` to a [Version]. If a string with an invalid format is provided, an exception will be
* thrown.
@ -19,8 +23,12 @@ data class Version(val type: ReleaseType, val major: Int, val minor: Int, val pa
Version(ReleaseType.FINAL, intParts[0], intParts[1], intParts[2])
} else if (parts.size == 2) {
val versionType = releaseTypeStringToValue(parts[0])
val intParts = ensureMinimumVersionParts(parts[1].split('.').map { it.toInt() })
Version(versionType, intParts[0], intParts[1], intParts[2])
if (versionType == ReleaseType.NIGHTLY) {
Nightly
} else {
val intParts = ensureMinimumVersionParts(parts[1].split('.').map { it.toInt() })
Version(versionType, intParts[0], intParts[1], intParts[2])
}
} else {
throw Exception("Invalid version string format")
}
@ -77,6 +85,7 @@ data class Version(val type: ReleaseType, val major: Int, val minor: Int, val pa
ReleaseType.ALPHA -> "alpha"
ReleaseType.BETA -> "beta"
ReleaseType.FINAL -> ""
ReleaseType.NIGHTLY -> return "nightly"
}
return "$typeString${if (typeString.isEmpty()) "" else "-"}$major.$minor.$patch"
}

View File

@ -1,10 +1,9 @@
package me.magnum.melonds.domain.repositories
import io.reactivex.Maybe
import me.magnum.melonds.domain.model.appupdate.AppUpdate
interface UpdatesRepository {
fun checkNewUpdate(): Maybe<AppUpdate>
suspend fun checkNewUpdate(): Result<AppUpdate?>
fun skipUpdate(update: AppUpdate)
fun notifyUpdateDownloaded(update: AppUpdate)
}

View File

@ -1,9 +1,9 @@
package me.magnum.melonds.domain.services
import io.reactivex.Observable
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import kotlinx.coroutines.flow.Flow
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.appupdate.AppUpdate
interface UpdateInstallManager {
fun downloadAndInstallUpdate(update: AppUpdate): Observable<DownloadProgress>
fun downloadAndInstallUpdate(update: AppUpdate): Flow<DownloadProgress>
}

View File

@ -26,7 +26,12 @@ import me.magnum.melonds.R
import me.magnum.melonds.common.Permission
import me.magnum.melonds.common.contracts.DirectoryPickerContract
import me.magnum.melonds.databinding.ActivityRomListBinding
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.domain.model.ConfigurationDirResult
import me.magnum.melonds.domain.model.ConsoleType
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.SortingMode
import me.magnum.melonds.domain.model.Version
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.ui.dsiwaremanager.DSiWareManagerActivity
import me.magnum.melonds.ui.emulator.EmulatorActivity
@ -98,14 +103,22 @@ class RomListActivity : AppCompatActivity() {
}
}
updatesViewModel.getAppUpdate().observe(this) {
when (it.type) {
AppUpdate.Type.PRODUCTION -> showProdUpdateAvailableDialog(it)
AppUpdate.Type.NIGHTLY -> showNightlyUpdateAvailableDialog(it)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
updatesViewModel.appUpdate.collectLatest {
when (it.type) {
AppUpdate.Type.PRODUCTION -> showProdUpdateAvailableDialog(it)
AppUpdate.Type.NIGHTLY -> showNightlyUpdateAvailableDialog(it)
}
}
}
}
updatesViewModel.getDownloadProgress().observe(this) {
onDownloadProgressUpdated(it)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
updatesViewModel.updateDownloadProgressEvent.collectLatest {
onDownloadProgressUpdated(it)
}
}
}
}
@ -282,6 +295,7 @@ class RomListActivity : AppCompatActivity() {
Version.ReleaseType.ALPHA -> getString(R.string.version_alpha)
Version.ReleaseType.BETA -> getString(R.string.version_beta)
Version.ReleaseType.FINAL -> ""
Version.ReleaseType.NIGHTLY -> return getString(R.string.version_nightly)
}
return "$typeString${if (typeString.isEmpty()) "" else " "}${version.major}.${version.minor}.${version.patch}"
}

View File

@ -1,65 +1,52 @@
package me.magnum.melonds.ui.romlist
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.disposables.CompositeDisposable
import me.magnum.melonds.common.Schedulers
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.repositories.UpdatesRepository
import me.magnum.melonds.domain.services.UpdateInstallManager
import me.magnum.melonds.extensions.addTo
import me.magnum.melonds.utils.SingleLiveEvent
import javax.inject.Inject
@HiltViewModel
class UpdatesViewModel @Inject constructor(
private val updatesRepository: UpdatesRepository,
private val updateInstallManager: UpdateInstallManager,
private val schedulers: Schedulers
) : ViewModel() {
private val disposables = CompositeDisposable()
private val appUpdateLiveData = SingleLiveEvent<AppUpdate>()
private val updateDownloadProgressLiveData = SingleLiveEvent<DownloadProgress>()
private val _appUpdate = Channel<AppUpdate>(Channel.CONFLATED)
val appUpdate = _appUpdate.receiveAsFlow()
private val _updateDownloadProgressEvent = Channel<DownloadProgress>(Channel.CONFLATED)
val updateDownloadProgressEvent = _updateDownloadProgressEvent.receiveAsFlow()
init {
updatesRepository.checkNewUpdate()
.onErrorComplete()
.subscribeOn(schedulers.backgroundThreadScheduler)
.subscribe {
appUpdateLiveData.postValue(it)
viewModelScope.launch {
updatesRepository.checkNewUpdate().map {
if (it != null) {
_appUpdate.send(it)
}
}
.addTo(disposables)
}
fun getAppUpdate(): LiveData<AppUpdate> {
return appUpdateLiveData
}
fun getDownloadProgress(): LiveData<DownloadProgress> {
return updateDownloadProgressLiveData
}
}
fun downloadUpdate(update: AppUpdate) {
updateInstallManager.downloadAndInstallUpdate(update)
.subscribeOn(schedulers.backgroundThreadScheduler)
.subscribe {
updateDownloadProgressLiveData.postValue(it)
viewModelScope.launch {
updateInstallManager.downloadAndInstallUpdate(update).collectLatest {
_updateDownloadProgressEvent.send(it)
if (it is DownloadProgress.DownloadComplete) {
updatesRepository.notifyUpdateDownloaded(update)
}
}
.addTo(disposables)
}
}
fun skipUpdate(update: AppUpdate) {
updatesRepository.skipUpdate(update)
}
override fun onCleared() {
super.onCleared()
disposables.clear()
}
}

View File

@ -21,6 +21,7 @@
<string name="version_alpha">Alpha</string>
<string name="version_beta">Beta</string>
<string name="version_nightly">Nightly</string>
<string name="console_ds">DS</string>
<string name="console_dsi">DSi (Experimental)</string>

View File

@ -1,12 +1,11 @@
package me.magnum.melonds.playstore
import io.reactivex.Maybe
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.repositories.UpdatesRepository
class PlayStoreUpdatesRepository : UpdatesRepository {
override fun checkNewUpdate(): Maybe<AppUpdate> {
return Maybe.empty()
override suspend fun checkNewUpdate(): Result<AppUpdate?> {
return Result.success(null)
}
override fun skipUpdate(update: AppUpdate) {

View File

@ -1,12 +1,13 @@
package me.magnum.melonds.services
import io.reactivex.Observable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import me.magnum.melonds.domain.model.DownloadProgress
import me.magnum.melonds.domain.model.appupdate.AppUpdate
import me.magnum.melonds.domain.services.UpdateInstallManager
class PlayStoreUpdateInstallManager : UpdateInstallManager {
override fun downloadAndInstallUpdate(update: AppUpdate): Observable<DownloadProgress> {
return Observable.error(UnsupportedOperationException("Cannot automatically update from PlayStore"))
override fun downloadAndInstallUpdate(update: AppUpdate): Flow<DownloadProgress> {
return emptyFlow()
}
}

View File

@ -99,6 +99,13 @@ class VersionTest {
assertEquals(3, version.patch)
}
@Test
fun testNightlyFromString() {
val version = Version.fromString("nightly-release")
assertEquals(Version.Nightly, version)
}
@Test
fun testInvalidFromString() {
try {