From 72ef10744a21a3d601ac50661edd7b3af4d75bf6 Mon Sep 17 00:00:00 2001 From: rumboalla Date: Wed, 27 Mar 2024 08:52:10 +0100 Subject: [PATCH] Download Progress * Disentangle MainViewModel. * Add Download Progress for Play Source and GitHub source. * Improve play token refresh. * TV UI is now default. * ApkMirror is disabled by default. * Random delay on Alarm is reduced to -5 +5 if ApkMirror is disabled. --- .../com/apkupdater/data/snack/ISnack.kt | 3 - .../com/apkupdater/data/snack/InstallSnack.kt | 9 --- .../com/apkupdater/data/snack/TextSnack.kt | 12 ++++ .../apkupdater/data/ui/AppInstallProgress.kt | 3 + .../com/apkupdater/data/ui/AppUpdate.kt | 11 +++ .../kotlin/com/apkupdater/data/ui/Link.kt | 6 +- .../kotlin/com/apkupdater/di/MainModule.kt | 27 ++++++-- .../main/kotlin/com/apkupdater/prefs/Prefs.kt | 7 +- .../apkupdater/repository/GitHubRepository.kt | 18 ++--- .../apkupdater/repository/PlayRepository.kt | 38 +++++------ .../apkupdater/ui/component/TvComponents.kt | 8 ++- .../com/apkupdater/ui/screen/MainScreen.kt | 67 ++++++------------- .../main/kotlin/com/apkupdater/util/Badger.kt | 31 +++++++++ .../kotlin/com/apkupdater/util/Extensions.kt | 8 ++- .../kotlin/com/apkupdater/util/InstallLog.kt | 22 ++++++ .../com/apkupdater/util/SessionInstaller.kt | 27 ++++++-- .../kotlin/com/apkupdater/util/SnackBar.kt | 21 ++++++ .../kotlin/com/apkupdater/util/Stringer.kt | 10 +++ .../main/kotlin/com/apkupdater/util/Themer.kt | 15 +++++ .../com/apkupdater/viewmodel/AppsViewModel.kt | 11 +-- .../apkupdater/viewmodel/InstallViewModel.kt | 61 +++++++++++------ .../com/apkupdater/viewmodel/MainViewModel.kt | 49 +++----------- .../apkupdater/viewmodel/SearchViewModel.kt | 29 +++++--- .../apkupdater/viewmodel/SettingsViewModel.kt | 9 +-- .../apkupdater/viewmodel/UpdatesViewModel.kt | 25 +++++-- .../com/apkupdater/worker/UpdatesWorker.kt | 5 +- 26 files changed, 343 insertions(+), 189 deletions(-) delete mode 100644 app/src/main/kotlin/com/apkupdater/data/snack/ISnack.kt delete mode 100644 app/src/main/kotlin/com/apkupdater/data/snack/InstallSnack.kt create mode 100644 app/src/main/kotlin/com/apkupdater/data/snack/TextSnack.kt create mode 100644 app/src/main/kotlin/com/apkupdater/data/ui/AppInstallProgress.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/Badger.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/InstallLog.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/SnackBar.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/Stringer.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/Themer.kt diff --git a/app/src/main/kotlin/com/apkupdater/data/snack/ISnack.kt b/app/src/main/kotlin/com/apkupdater/data/snack/ISnack.kt deleted file mode 100644 index 45e84dec..00000000 --- a/app/src/main/kotlin/com/apkupdater/data/snack/ISnack.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.apkupdater.data.snack - -interface ISnack diff --git a/app/src/main/kotlin/com/apkupdater/data/snack/InstallSnack.kt b/app/src/main/kotlin/com/apkupdater/data/snack/InstallSnack.kt deleted file mode 100644 index 16bd06fb..00000000 --- a/app/src/main/kotlin/com/apkupdater/data/snack/InstallSnack.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.apkupdater.data.snack - -import androidx.annotation.StringRes - -data class InstallSnack(val success: Boolean, val name: String): ISnack - -data class TextSnack(val message: String): ISnack - -data class TextIdSnack(@StringRes val id: Int): ISnack diff --git a/app/src/main/kotlin/com/apkupdater/data/snack/TextSnack.kt b/app/src/main/kotlin/com/apkupdater/data/snack/TextSnack.kt new file mode 100644 index 00000000..416a5041 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/data/snack/TextSnack.kt @@ -0,0 +1,12 @@ +package com.apkupdater.data.snack + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarVisuals + + +class TextSnack( + override val message: String, + override val actionLabel: String? = null, + override val duration: SnackbarDuration = SnackbarDuration.Short, + override val withDismissAction: Boolean = true +): SnackbarVisuals diff --git a/app/src/main/kotlin/com/apkupdater/data/ui/AppInstallProgress.kt b/app/src/main/kotlin/com/apkupdater/data/ui/AppInstallProgress.kt new file mode 100644 index 00000000..ea3aaee8 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/data/ui/AppInstallProgress.kt @@ -0,0 +1,3 @@ +package com.apkupdater.data.ui + +data class AppInstallProgress(val id: Int, val progress: Long = 0L, val total: Long = 0L) diff --git a/app/src/main/kotlin/com/apkupdater/data/ui/AppUpdate.kt b/app/src/main/kotlin/com/apkupdater/data/ui/AppUpdate.kt index 0c16a753..d53dab6f 100644 --- a/app/src/main/kotlin/com/apkupdater/data/ui/AppUpdate.kt +++ b/app/src/main/kotlin/com/apkupdater/data/ui/AppUpdate.kt @@ -14,6 +14,8 @@ data class AppUpdate( val link: Link = Link.Empty, val whatsNew: String = "", val isInstalling: Boolean = false, + val total: Long = 0L, + val progress: Long = 0L, val id: Int = "${source.name}.$packageName.$versionCode.$version".hashCode() ) @@ -32,3 +34,12 @@ fun MutableList.removeId(id: Int): List { if (index != -1) this.removeAt(index) return this } + +fun MutableList.setProgress(progress: AppInstallProgress): MutableList { + val index = this.indexOf(progress.id) + if (index != -1) { + if (progress.progress != 0L) this[index] = this[index].copy(progress = progress.progress) + if (progress.total != 0L) this[index] = this[index].copy(total = progress.total) + } + return this +} diff --git a/app/src/main/kotlin/com/apkupdater/data/ui/Link.kt b/app/src/main/kotlin/com/apkupdater/data/ui/Link.kt index 5c7e1777..0399be82 100644 --- a/app/src/main/kotlin/com/apkupdater/data/ui/Link.kt +++ b/app/src/main/kotlin/com/apkupdater/data/ui/Link.kt @@ -1,9 +1,11 @@ package com.apkupdater.data.ui +import com.aurora.gplayapi.data.models.File + sealed class Link { data object Empty: Link() - data class Url(val link: String): Link() + data class Url(val link: String, val size: Long = 0L): Link() data class Xapk(val link: String): Link() - data class Play(val getInstallFiles: () -> List): Link() + data class Play(val getInstallFiles: () -> List): Link() } diff --git a/app/src/main/kotlin/com/apkupdater/di/MainModule.kt b/app/src/main/kotlin/com/apkupdater/di/MainModule.kt index 10bac5b2..06c5f974 100644 --- a/app/src/main/kotlin/com/apkupdater/di/MainModule.kt +++ b/app/src/main/kotlin/com/apkupdater/di/MainModule.kt @@ -22,9 +22,14 @@ import com.apkupdater.service.AptoideService import com.apkupdater.service.FdroidService import com.apkupdater.service.GitHubService import com.apkupdater.service.GitLabService +import com.apkupdater.util.Badger import com.apkupdater.util.Clipboard import com.apkupdater.util.Downloader +import com.apkupdater.util.InstallLog import com.apkupdater.util.SessionInstaller +import com.apkupdater.util.SnackBar +import com.apkupdater.util.Stringer +import com.apkupdater.util.Themer import com.apkupdater.util.UpdatesNotification import com.apkupdater.util.addUserAgentInterceptor import com.apkupdater.util.isAndroidTv @@ -164,16 +169,26 @@ val mainModule = module { single { Clipboard(androidContext()) } - single { SessionInstaller(get()) } + single { SessionInstaller(get(), get()) } - viewModel { MainViewModel(get()) } + single { SnackBar() } - viewModel { parameters -> AppsViewModel(parameters.get(), get(), get()) } + single { Badger() } - viewModel { parameters -> UpdatesViewModel(parameters.get(), get(), get(), get(), get()) } + single { Themer(get()) } - viewModel { parameters -> SettingsViewModel(parameters.get(), get(), get(), WorkManager.getInstance(get()), get(), get()) } + single { Stringer(androidContext()) } - viewModel { parameters -> SearchViewModel(parameters.get(), get(), get(), get(), get()) } + single { InstallLog() } + + viewModel { MainViewModel(get(), get()) } + + viewModel { AppsViewModel(get(), get(), get()) } + + viewModel { UpdatesViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } + + viewModel { SettingsViewModel(get(), get(), WorkManager.getInstance(get()), get(), get(), get(), get()) } + + viewModel { SearchViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt b/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt index 1f19e91b..0b409876 100644 --- a/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt +++ b/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt @@ -18,12 +18,12 @@ class Prefs( val excludeStore = boolean("excludeStore", defValue = false, backed = true) val portraitColumns = int("portraitColumns", 3, true) val landscapeColumns = int("landscapeColumns", 6, true) - val playTextAnimations = boolean("playTextAnimaions", defValue = true, backed = true) + val playTextAnimations = boolean("playTextAnimations", defValue = true, backed = true) val ignoreAlpha = boolean("ignoreAlpha", defValue = true, backed = true) val ignoreBeta = boolean("ignoreBeta", defValue = true, backed = true) val ignorePreRelease = boolean("ignorePreRelease", defValue = true, backed = true) val useSafeStores = boolean("useSafeStores", defValue = true, backed = true) - val useApkMirror = boolean("useApkMirror", defValue = !isAndroidTv, backed = true) + val useApkMirror = boolean("useApkMirror", defValue = false, backed = true) val useGitHub = boolean("useGitHub", defValue = true, backed = true) val useGitLab = boolean("useGitLab", defValue = true, backed = true) val useFdroid = boolean("useFdroid", defValue = true, backed = true) @@ -34,9 +34,10 @@ class Prefs( val enableAlarm = boolean("enableAlarm", defValue = false, backed = true) val alarmHour = int("alarmHour", defValue = 12, backed = true) val alarmFrequency = int("alarmFrequency", 0, backed = true) - val androidTvUi = boolean("androidTvUi", defValue = isAndroidTv, backed = true) + val androidTvUi = boolean("androidTvUi", defValue = true, backed = true) val rootInstall = boolean("rootInstall", defValue = false, backed = true) val theme = int("theme", defValue = 0, backed = true) val lastTab = string("lastTab", defValue = Screen.Updates.route, backed = true) val playAuthData = json("playAuthData", AuthData("", ""), true) + val lastPlayCheck = long("lastPlayCheck", 0L, true) } diff --git a/app/src/main/kotlin/com/apkupdater/repository/GitHubRepository.kt b/app/src/main/kotlin/com/apkupdater/repository/GitHubRepository.kt index 44fb88b6..5d717ed2 100644 --- a/app/src/main/kotlin/com/apkupdater/repository/GitHubRepository.kt +++ b/app/src/main/kotlin/com/apkupdater/repository/GitHubRepository.kt @@ -121,7 +121,7 @@ class GitHubRepository( versionCode = 0L, oldVersionCode = app?.versionCode ?: 0L, source = GitHubSource, - link = Link.Url(findApkAssetArch(releases[0].assets, extra)), + link = findApkAssetArch(releases[0].assets, extra).let { Link.Url(it.browser_download_url, it.size) }, whatsNew = releases[0].body, iconUri = if (apps == null) Uri.parse(releases[0].author.avatar_url) else Uri.EMPTY ))) @@ -154,20 +154,20 @@ class GitHubRepository( private fun findApkAssetArch( assets: List, extra: Regex? - ): String { + ): GitHubReleaseAsset { val apks = assets .filter { it.browser_download_url.endsWith(".apk", true) } .filter { filterExtra(it, extra) } when { - apks.isEmpty() -> return "" - apks.size == 1 -> return apks.first().browser_download_url + apks.isEmpty() -> return GitHubReleaseAsset(0L, "") + apks.size == 1 -> return apks.first() else -> { // Try to match exact arch Build.SUPPORTED_ABIS.forEach { arch -> apks.forEach { apk -> if (apk.browser_download_url.contains(arch, true)) { - return apk.browser_download_url + return apk } } } @@ -175,7 +175,7 @@ class GitHubRepository( if (Build.SUPPORTED_ABIS.contains("arm64-v8a")) { apks.forEach { apk -> if (apk.browser_download_url.contains("arm64", true)) { - return apk.browser_download_url + return apk } } } @@ -183,7 +183,7 @@ class GitHubRepository( if (Build.SUPPORTED_ABIS.contains("x86_64")) { apks.forEach { apk -> if (apk.browser_download_url.contains("x64", true)) { - return apk.browser_download_url + return apk } } } @@ -191,12 +191,12 @@ class GitHubRepository( if (Build.SUPPORTED_ABIS.contains("armeabi-v7a")) { apks.forEach { apk -> if (apk.browser_download_url.contains("arm", true)) { - return apk.browser_download_url + return apk } } } // If no match, return biggest apk in the hope it's universal - return apks.maxByOrNull { it.size }?.browser_download_url.orEmpty() + return apks.maxByOrNull { it.size } ?: GitHubReleaseAsset(0L, "") } } } diff --git a/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt b/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt index 98624adc..fa487bba 100644 --- a/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt +++ b/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt @@ -20,18 +20,10 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.google.gson.Gson -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -@OptIn(FlowPreview::class) class PlayRepository( private val context: Context, private val gson: Gson, @@ -41,16 +33,8 @@ class PlayRepository( const val AUTH_URL = "https://auroraoss.com/api/auth" } - init { - // TODO: Needs testing. - PlayHttpClient.responseCode - .filter { it == 401 } - .debounce(60 * 5 * 1_000) - .onEach { runCatching { refreshAuth() }.getOrNull() } - .launchIn(CoroutineScope(Dispatchers.IO)) - } - private fun refreshAuth(): AuthData { + Log.i("PlayRepository", "Refreshing token.") val properties = NativeDeviceInfoProvider(context).getNativeDeviceProperties() val playResponse = PlayHttpClient.postAuth(AUTH_URL, gson.toJson(properties).toByteArray()) if (playResponse.isSuccessful) { @@ -66,6 +50,21 @@ class PlayRepository( if (savedData.email.isEmpty()) { return refreshAuth() } + if (System.currentTimeMillis() - prefs.lastPlayCheck.get() > 60 * 60 * 1_000) { + // Update check time + prefs.lastPlayCheck.put(System.currentTimeMillis()) + Log.i("PlayRepository", "Checking token validity.") + + // 1h has passed check if token still works + val app = AppDetailsHelper(savedData) + .using(PlayHttpClient) + .getAppByPackageName("com.google.android.gm") + + if (app.packageName.isEmpty()) { + return refreshAuth() + } + Log.i("PlayRepository", "Token still valid.") + } return savedData } @@ -77,7 +76,7 @@ class PlayRepository( .using(PlayHttpClient) .searchResults(text) .appList - .take(5) + .take(10) .map { it.toAppUpdate(::getInstallFiles) } emit(Result.success(updates)) } else { @@ -118,12 +117,11 @@ class PlayRepository( .using(PlayHttpClient) .purchase(app.packageName, app.versionCode, app.offerType) .filter { it.type == File.FileType.BASE || it.type == File.FileType.SPLIT } - .map { it.url } } fun App.toAppUpdate( - getInstallFiles: (App) -> List, + getInstallFiles: (App) -> List, oldVersion: String = "", oldVersionCode: Long = 0L ) = AppUpdate( diff --git a/app/src/main/kotlin/com/apkupdater/ui/component/TvComponents.kt b/app/src/main/kotlin/com/apkupdater/ui/component/TvComponents.kt index 83c0f844..ca88e9af 100644 --- a/app/src/main/kotlin/com/apkupdater/ui/component/TvComponents.kt +++ b/app/src/main/kotlin/com/apkupdater/ui/component/TvComponents.kt @@ -32,6 +32,7 @@ import com.apkupdater.data.ui.AppInstalled import com.apkupdater.data.ui.AppUpdate import com.apkupdater.data.ui.Source import com.apkupdater.util.getAppName +import com.apkupdater.util.to2f import com.apkupdater.util.toAnnotatedString @@ -79,7 +80,12 @@ fun TvInstallButton( onClick = { onInstall(app.packageName) } ) { if (app.isInstalling) { - CircularProgressIndicator(Modifier.size(24.dp)) + if (app.total != 0L && app.progress != 0L) { + val p = (app.progress.toFloat() / app.total) * 100f + Text("${p.to2f()}%") + } else { + CircularProgressIndicator(Modifier.size(24.dp)) + } } else { Text(stringResource(R.string.install_cd)) } diff --git a/app/src/main/kotlin/com/apkupdater/ui/screen/MainScreen.kt b/app/src/main/kotlin/com/apkupdater/ui/screen/MainScreen.kt index a0e192e4..326add5c 100644 --- a/app/src/main/kotlin/com/apkupdater/ui/screen/MainScreen.kt +++ b/app/src/main/kotlin/com/apkupdater/ui/screen/MainScreen.kt @@ -1,7 +1,6 @@ package com.apkupdater.ui.screen import android.app.Activity.RESULT_CANCELED -import android.content.Context import android.content.Intent import android.util.Log import androidx.activity.ComponentActivity @@ -15,12 +14,10 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.BadgedBox import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -30,7 +27,6 @@ import androidx.compose.material3.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,14 +41,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.apkupdater.R -import com.apkupdater.data.snack.ISnack -import com.apkupdater.data.snack.InstallSnack -import com.apkupdater.data.snack.TextIdSnack -import com.apkupdater.data.snack.TextSnack import com.apkupdater.data.ui.Screen import com.apkupdater.ui.component.BadgeText import com.apkupdater.ui.theme.AppTheme +import com.apkupdater.util.Badger +import com.apkupdater.util.InstallLog +import com.apkupdater.util.SnackBar +import com.apkupdater.util.Themer import com.apkupdater.viewmodel.AppsViewModel import com.apkupdater.viewmodel.MainViewModel import com.apkupdater.viewmodel.SearchViewModel @@ -63,18 +58,18 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.androidx.compose.get import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf import kotlin.coroutines.CoroutineContext @Composable fun MainScreen(mainViewModel: MainViewModel = koinViewModel()) { // ViewModels - val appsViewModel: AppsViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) }) - val updatesViewModel: UpdatesViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) }) - val searchViewModel: SearchViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) }) - val settingsViewModel: SettingsViewModel = koinViewModel(parameters = { parametersOf(mainViewModel) }) + val appsViewModel: AppsViewModel = koinViewModel() + val updatesViewModel: UpdatesViewModel = koinViewModel() + val searchViewModel: SearchViewModel = koinViewModel() + val settingsViewModel: SettingsViewModel = koinViewModel() // Navigation val navController = rememberNavController() @@ -89,9 +84,10 @@ fun MainScreen(mainViewModel: MainViewModel = koinViewModel()) { } // Used to launch the install intent and get dismissal result + val installLog = get() val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_CANCELED) { - mainViewModel.cancelCurrentInstall() + installLog.cancelCurrentInstall() } } @@ -102,14 +98,10 @@ fun MainScreen(mainViewModel: MainViewModel = koinViewModel()) { intentListener(mainViewModel, updatesViewModel, navController, launcher) // Theme - val theme = mainViewModel.theme.collectAsStateWithLifecycle().value + val theme = get().flow().collectAsStateWithLifecycle().value // SnackBar - val context = LocalContext.current - val snackBarHostState = remember { SnackbarHostState() } - mainViewModel.snackBar.CollectAsEffect { - handleSnack(context, snackBarHostState, it) - } + val snackBarHostState = handleSnackBar() AppTheme(theme) { Scaffold( @@ -129,35 +121,21 @@ fun MainScreen(mainViewModel: MainViewModel = koinViewModel()) { } } -suspend fun handleSnack( - context: Context, - snackBarHostState: SnackbarHostState, - snack: ISnack -) { - when (snack) { - is InstallSnack -> { - val message = if (snack.success) R.string.install_success else R.string.install_failure - snackBarHostState.showSnackbar( - context.getString(message, snack.name), - null, - true, - SnackbarDuration.Short - ) - } - is TextSnack -> snackBarHostState.showSnackbar(snack.message, null, true, SnackbarDuration.Short) - is TextIdSnack -> snackBarHostState.showSnackbar(context.getString(snack.id), null, true, SnackbarDuration.Short) - else -> Log.e("MainScreen", "Invalid snack type.") +@Composable +fun handleSnackBar(): SnackbarHostState { + val snackBarHostState = remember { SnackbarHostState() } + get().flow().CollectAsEffect(Dispatchers.IO) { + snackBarHostState.showSnackbar(it) } + return snackBarHostState } @Composable fun Flow.CollectAsEffect( context: CoroutineContext = Dispatchers.IO, block: suspend (T) -> Unit -) { - LaunchedEffect(Unit) { - onEach(block).flowOn(context).launchIn(this) - } +) = LaunchedEffect(Unit) { + onEach(block).flowOn(context).launchIn(this) } @Composable @@ -194,7 +172,7 @@ fun checkNotificationIntent( @Composable fun BottomBar(mainViewModel: MainViewModel, navController: NavController) = BottomAppBar { - val badges = mainViewModel.badges.collectAsState().value + val badges = get().flow().collectAsStateWithLifecycle().value mainViewModel.screens.forEach { screen -> val state = navController.currentBackStackEntryAsState().value val selected = state?.destination?.route == screen.route @@ -202,7 +180,6 @@ fun BottomBar(mainViewModel: MainViewModel, navController: NavController) = Bott } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RowScope.BottomBarItem( mainViewModel: MainViewModel, diff --git a/app/src/main/kotlin/com/apkupdater/util/Badger.kt b/app/src/main/kotlin/com/apkupdater/util/Badger.kt new file mode 100644 index 00000000..95be42ad --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/Badger.kt @@ -0,0 +1,31 @@ +package com.apkupdater.util + +import com.apkupdater.data.ui.Screen +import kotlinx.coroutines.flow.MutableStateFlow + + +class Badger { + + private val badges = MutableStateFlow(mapOf( + Screen.Apps.route to "", + Screen.Search.route to "", + Screen.Updates.route to "", + Screen.Settings.route to "" + )) + + fun flow() = badges + + fun changeSearchBadge(number: String) = changeBadge(Screen.Search.route, number) + + fun changeAppsBadge(number: String) = changeBadge(Screen.Apps.route, number) + + fun changeUpdatesBadge(number: String) = changeBadge(Screen.Updates.route, number) + + private fun changeBadge(route: String, number: String) { + val finalNumber = if (number.toIntOrNull() == 0) "" else number + val newBadges = badges.value.toMutableMap() + newBadges[route] = finalNumber + badges.value = newBadges + } + +} diff --git a/app/src/main/kotlin/com/apkupdater/util/Extensions.kt b/app/src/main/kotlin/com/apkupdater/util/Extensions.kt index 7edd4b10..eb957e02 100644 --- a/app/src/main/kotlin/com/apkupdater/util/Extensions.kt +++ b/app/src/main/kotlin/com/apkupdater/util/Extensions.kt @@ -34,14 +34,16 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.yield import okhttp3.OkHttpClient import java.security.MessageDigest +import java.text.DecimalFormatSymbols import java.util.Calendar +import java.util.Locale import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext // A clickable modifier that will disable the default ripple -fun Modifier.clickableNoRipple(onClick: () -> Unit) = +fun Modifier.clickableNoRipple(onClick: () -> Unit) = this. clickable(MutableInteractionSource(), null, onClick = onClick) // Launches a coroutine and executes it inside the mutex @@ -155,3 +157,7 @@ fun OkHttpClient.Builder.addUserAgentInterceptor(agent: String) = addNetworkInte fun filterVersionTag(version: String) = version .replace(Regex("^\\D*"), "") //.replace(Regex("\\D+\$"), "") // In case we want to remove non-numeric at end too + +fun Float.to2f() = String + .format("%.2f", this) + .replace('.', DecimalFormatSymbols.getInstance(Locale.getDefault()).decimalSeparator) diff --git a/app/src/main/kotlin/com/apkupdater/util/InstallLog.kt b/app/src/main/kotlin/com/apkupdater/util/InstallLog.kt new file mode 100644 index 00000000..9621e68a --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/InstallLog.kt @@ -0,0 +1,22 @@ +package com.apkupdater.util + +import com.apkupdater.data.ui.AppInstallProgress +import com.apkupdater.data.ui.AppInstallStatus +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + + +class InstallLog { + + private val status = MutableSharedFlow(100) + private val progress = MutableSharedFlow(100) + private var currentInstallLog: Int = 0 + + fun status() = status.asSharedFlow() + fun progress() = progress.asSharedFlow() + + fun cancelCurrentInstall() = status.tryEmit(AppInstallStatus(false, currentInstallLog, false)) + fun emitStatus(newStatus: AppInstallStatus) = status.tryEmit(newStatus) + fun emitProgress(newProgress: AppInstallProgress) = progress.tryEmit(newProgress) + +} diff --git a/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt b/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt index f4379b4b..12979201 100644 --- a/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt +++ b/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt @@ -10,18 +10,23 @@ import android.os.Build import android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES import androidx.core.content.ContextCompat.startActivity import com.apkupdater.BuildConfig +import com.apkupdater.data.ui.AppInstallProgress import com.apkupdater.ui.activity.MainActivity import com.topjohnwu.superuser.Shell import java.io.File import java.io.InputStream +import java.io.OutputStream import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.ZipFile -class SessionInstaller(private val context: Context) { +class SessionInstaller( + private val context: Context, + private val installLog: InstallLog +) { companion object { - const val InstallAction = "installAction" + const val INSTALL_ACTION = "installAction" } private val installMutex = AtomicBoolean(false) @@ -47,17 +52,18 @@ class SessionInstaller(private val context: Context) { } val sessionId = packageInstaller.createSession(params) + var bytes = 0L packageInstaller.openSession(sessionId).use { session -> streams.forEach { session.openWrite("$packageName.${randomUUID()}", 0, -1).use { output -> - it.copyTo(output) + bytes += it.copyToAndNotify(output, id, installLog, bytes) it.close() session.fsync(output) } } val intent = Intent(context, MainActivity::class.java).apply { - action = "$InstallAction.$id" + action = "$INSTALL_ACTION.$id" } installMutex.lock() @@ -114,3 +120,16 @@ class SessionInstaller(private val context: Context) { install(id, packageName, streams) } + +fun InputStream.copyToAndNotify(out: OutputStream, id: Int, installLog: InstallLog, total: Long, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long { + var bytesCopied: Long = 0 + val buffer = ByteArray(bufferSize) + var bytes = read(buffer) + while (bytes >= 0) { + out.write(buffer, 0, bytes) + bytesCopied += bytes + installLog.emitProgress(AppInstallProgress(id, progress = total + bytesCopied)) + bytes = read(buffer) + } + return bytesCopied +} diff --git a/app/src/main/kotlin/com/apkupdater/util/SnackBar.kt b/app/src/main/kotlin/com/apkupdater/util/SnackBar.kt new file mode 100644 index 00000000..a55883f2 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/SnackBar.kt @@ -0,0 +1,21 @@ +package com.apkupdater.util + +import androidx.compose.material3.SnackbarVisuals +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch + +class SnackBar { + + private val snackBars = MutableSharedFlow() + + fun flow(): SharedFlow = snackBars + + fun snackBar( + scope: CoroutineScope = CoroutineScope(Dispatchers.IO), + message: SnackbarVisuals + ) = scope.launch { snackBars.emit(message) } + +} diff --git a/app/src/main/kotlin/com/apkupdater/util/Stringer.kt b/app/src/main/kotlin/com/apkupdater/util/Stringer.kt new file mode 100644 index 00000000..7657a81d --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/Stringer.kt @@ -0,0 +1,10 @@ +package com.apkupdater.util + +import android.content.Context + +class Stringer(val context: Context) { + + fun get(id: Int) = context.getString(id) + fun get(id: Int, vararg params: Any?) = context.getString(id, *params) + +} diff --git a/app/src/main/kotlin/com/apkupdater/util/Themer.kt b/app/src/main/kotlin/com/apkupdater/util/Themer.kt new file mode 100644 index 00000000..a03268d0 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/Themer.kt @@ -0,0 +1,15 @@ +package com.apkupdater.util + +import com.apkupdater.prefs.Prefs +import com.apkupdater.ui.theme.isDarkTheme +import kotlinx.coroutines.flow.MutableStateFlow + +class Themer(prefs: Prefs) { + + private val theme = MutableStateFlow(isDarkTheme(prefs.theme.get())) + + fun flow() = theme + + fun setTheme(v: Boolean) { theme.value = v } + +} diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/AppsViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/AppsViewModel.kt index 3f5f0638..9df09e28 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/AppsViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/AppsViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.apkupdater.data.ui.AppsUiState import com.apkupdater.prefs.Prefs import com.apkupdater.repository.AppsRepository +import com.apkupdater.util.Badger import com.apkupdater.util.launchWithMutex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -13,9 +14,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.sync.Mutex class AppsViewModel( - private val mainViewModel: MainViewModel, private val repository: AppsRepository, - private val prefs: Prefs + private val prefs: Prefs, + private val badger: Badger ) : ViewModel() { private val mutex = Mutex() @@ -25,7 +26,7 @@ class AppsViewModel( fun refresh(load: Boolean = true) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { if (load) state.value = buildLoadingState() - mainViewModel.changeAppsBadge("") + badger.changeAppsBadge("") repository.getApps().collect { it.onSuccess { apps -> state.value = AppsUiState.Success( @@ -34,10 +35,10 @@ class AppsViewModel( prefs.excludeStore.get(), prefs.excludeDisabled.get() ) - mainViewModel.changeAppsBadge(apps.size.toString()) + badger.changeAppsBadge(apps.size.toString()) }.onFailure { ex -> state.value = AppsUiState.Error - mainViewModel.changeAppsBadge("!") + badger.changeAppsBadge("!") Log.e("InstalledViewModel", "Error getting apps.", ex) } } diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt index eba94522..eef11109 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt @@ -5,25 +5,30 @@ import androidx.compose.ui.platform.UriHandler import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.apkupdater.R -import com.apkupdater.data.snack.InstallSnack -import com.apkupdater.data.snack.TextIdSnack +import com.apkupdater.data.snack.TextSnack import com.apkupdater.data.ui.ApkMirrorSource +import com.apkupdater.data.ui.AppInstallProgress import com.apkupdater.data.ui.AppInstallStatus import com.apkupdater.data.ui.AppUpdate import com.apkupdater.data.ui.Link import com.apkupdater.prefs.Prefs import com.apkupdater.util.Downloader +import com.apkupdater.util.InstallLog import com.apkupdater.util.SessionInstaller -import kotlinx.coroutines.Dispatchers +import com.apkupdater.util.SnackBar +import com.apkupdater.util.Stringer import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach abstract class InstallViewModel( - private val mainViewModel: MainViewModel, private val downloader: Downloader, private val installer: SessionInstaller, - private val prefs: Prefs + private val prefs: Prefs, + private val snackBar: SnackBar, + private val stringer: Stringer, + private val installLog: InstallLog ): ViewModel() { fun install(update: AppUpdate, uriHandler: UriHandler) { @@ -39,18 +44,22 @@ abstract class InstallViewModel( } } - protected fun subscribeToInstallLog( + protected fun subscribeToInstallStatus( block: (AppInstallStatus) -> Unit - ) = viewModelScope.launch(Dispatchers.IO) { - mainViewModel.appInstallLog.collect { - block(it) - if (it.success) { - finishInstall(it.id).join() - } else { - cancelInstall(it.id).join() - } + ) = installLog.status().onEach { + block(it) + if (it.success) { + finishInstall(it.id).join() + } else { + cancelInstall(it.id).join() } - } + }.launchIn(viewModelScope) + + protected fun subscribeToInstallProgress( + block: (AppInstallProgress) -> Unit + ) = installLog.progress().onEach { + block(it) + }.launchIn(viewModelScope) protected fun downloadAndRootInstall(id: Int, link: Link) = runCatching { when (link) { @@ -61,9 +70,11 @@ abstract class InstallViewModel( cancelInstall(id) } } - else -> { mainViewModel.sendSnack(TextIdSnack(R.string.root_install_not_supported))} + else -> snackBar.snackBar( + viewModelScope, + TextSnack(stringer.get(R.string.root_install_not_supported)) + ) } - }.getOrElse { Log.e("InstallViewModel", "Error in downloadAndRootInstall.", it) cancelInstall(id) @@ -72,8 +83,15 @@ abstract class InstallViewModel( protected suspend fun downloadAndInstall(id: Int, packageName: String, link: Link) = runCatching { when (link) { Link.Empty -> { Log.e("InstallViewModel", "downloadAndInstall: Unsupported.")} - is Link.Play -> installer.playInstall(id, packageName, link.getInstallFiles().map { downloader.downloadStream(it)!! }) - is Link.Url -> installer.install(id, packageName, downloader.downloadStream(link.link)!!) + is Link.Play -> { + val files = link.getInstallFiles() + installLog.emitProgress(AppInstallProgress(id, 0L, files.sumOf { it.size })) + installer.playInstall(id, packageName, files.map { downloader.downloadStream(it.url)!! }) + } + is Link.Url -> { + installLog.emitProgress(AppInstallProgress(id, 0L, link.size)) + installer.install(id, packageName, downloader.downloadStream(link.link)!!) + } is Link.Xapk -> installer.installXapk(id, packageName, downloader.downloadStream(link.link)!!) } }.getOrElse { @@ -84,7 +102,8 @@ abstract class InstallViewModel( protected fun sendInstallSnack(updates: List, log: AppInstallStatus) { if (log.snack) { updates.find { log.id == it.id }?.let { app -> - mainViewModel.sendSnack(InstallSnack(log.success, app.name)) + val message = if (log.success) R.string.install_success else R.string.install_failure + snackBar.snackBar(viewModelScope, TextSnack(stringer.get(message, app.name))) } } } diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/MainViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/MainViewModel.kt index 098646a8..ae012495 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/MainViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/MainViewModel.kt @@ -9,39 +9,29 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination -import com.apkupdater.data.snack.ISnack import com.apkupdater.data.ui.AppInstallStatus import com.apkupdater.data.ui.Screen import com.apkupdater.prefs.Prefs -import com.apkupdater.ui.theme.isDarkTheme +import com.apkupdater.util.InstallLog import com.apkupdater.util.SessionInstaller import com.apkupdater.util.UpdatesNotification import com.apkupdater.util.getAppId import com.apkupdater.util.getIntentExtra import com.apkupdater.util.orFalse import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -class MainViewModel(private val prefs: Prefs) : ViewModel() { +class MainViewModel( + private val prefs: Prefs, + private val installLog: InstallLog +) : ViewModel() { val screens = listOf(Screen.Apps, Screen.Search, Screen.Updates, Screen.Settings) - val theme = MutableStateFlow(isDarkTheme(prefs.theme.get())) - - val badges = MutableStateFlow(mapOf( - Screen.Apps.route to "", - Screen.Search.route to "", - Screen.Updates.route to "", - Screen.Settings.route to "" - )) - - val snackBar = MutableSharedFlow() - val isRefreshing = MutableStateFlow(false) - val appInstallLog = MutableSharedFlow() + private var currentInstallId = 0 fun refresh( @@ -55,20 +45,6 @@ class MainViewModel(private val prefs: Prefs) : ViewModel() { } } - fun sendSnack(snack: ISnack) = viewModelScope.launch { snackBar.emit(snack) } - - fun setTheme(theme: Boolean) = this.theme.apply { value = theme } - - fun changeSearchBadge(number: String) = changeBadge(Screen.Search.route, number) - - fun changeAppsBadge(number: String) = changeBadge(Screen.Apps.route, number) - - fun changeUpdatesBadge(number: String) = changeBadge(Screen.Updates.route, number) - - fun cancelCurrentInstall() = viewModelScope.launch(Dispatchers.IO) { - appInstallLog.emit(AppInstallStatus(false, currentInstallId, false)) - } - fun processIntent( intent: Intent, launcher: ManagedActivityResultLauncher, @@ -77,7 +53,7 @@ class MainViewModel(private val prefs: Prefs) : ViewModel() { ) { when { intent.action == UpdatesNotification.UpdateAction -> processUpdateIntent(navController, updatesViewModel) - intent.action?.contains(SessionInstaller.InstallAction).orFalse() -> processInstallIntent(intent, launcher) + intent.action?.contains(SessionInstaller.INSTALL_ACTION).orFalse() -> processInstallIntent(intent, launcher) else -> {} } } @@ -106,13 +82,13 @@ class MainViewModel(private val prefs: Prefs) : ViewModel() { } PackageInstaller.STATUS_SUCCESS -> { intent.getAppId()?.let { - appInstallLog.emit(AppInstallStatus(true, it)) + installLog.emitStatus(AppInstallStatus(true, it)) } } else -> { // We assume error and cancel the install intent.getAppId()?.let { - appInstallLog.emit(AppInstallStatus(false, it)) + installLog.emitStatus(AppInstallStatus(false, it)) } val message = intent.extras?.getString(PackageInstaller.EXTRA_STATUS_MESSAGE) Log.e("MainViewModel", "Failed to install app: $message $intent") @@ -128,11 +104,4 @@ class MainViewModel(private val prefs: Prefs) : ViewModel() { updatesViewModel.refresh() } - private fun changeBadge(route: String, number: String) { - val finalNumber = if (number.toIntOrNull() == 0) "" else number - val newBadges = badges.value.toMutableMap() - newBadges[route] = finalNumber - badges.value = newBadges - } - } diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/SearchViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/SearchViewModel.kt index 94dd6780..ad786794 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/SearchViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/SearchViewModel.kt @@ -5,10 +5,15 @@ import com.apkupdater.data.ui.AppUpdate import com.apkupdater.data.ui.SearchUiState import com.apkupdater.data.ui.removeId import com.apkupdater.data.ui.setIsInstalling +import com.apkupdater.data.ui.setProgress import com.apkupdater.prefs.Prefs import com.apkupdater.repository.SearchRepository +import com.apkupdater.util.Badger import com.apkupdater.util.Downloader +import com.apkupdater.util.InstallLog import com.apkupdater.util.SessionInstaller +import com.apkupdater.util.SnackBar +import com.apkupdater.util.Stringer import com.apkupdater.util.launchWithMutex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -19,20 +24,26 @@ import kotlinx.coroutines.sync.Mutex class SearchViewModel( - private val mainViewModel: MainViewModel, private val searchRepository: SearchRepository, private val installer: SessionInstaller, + private val badger: Badger, downloader: Downloader, - prefs: Prefs -) : InstallViewModel(mainViewModel, downloader, installer, prefs) { + prefs: Prefs, + snackBar: SnackBar, + stringer: Stringer, + installLog: InstallLog +) : InstallViewModel(downloader, installer, prefs, snackBar, stringer, installLog) { private val mutex = Mutex() private val state = MutableStateFlow(SearchUiState.Success(emptyList())) private var job: Job? = null init { - subscribeToInstallLog { log -> - sendInstallSnack(state.value.updates(), log) + subscribeToInstallStatus { status -> + sendInstallSnack(state.value.updates(), status) + } + subscribeToInstallProgress { progress -> + state.value = SearchUiState.Success(state.value.mutableUpdates().setProgress(progress)) } } @@ -45,13 +56,13 @@ class SearchViewModel( private fun searchJob(text: String) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { state.value = SearchUiState.Loading - mainViewModel.changeSearchBadge("") + badger.changeSearchBadge("") searchRepository.search(text).collect { it.onSuccess { apps -> state.value = SearchUiState.Success(apps) - mainViewModel.changeSearchBadge(apps.size.toString()) + badger.changeSearchBadge(apps.size.toString()) }.onFailure { - mainViewModel.changeSearchBadge("!") + badger.changeSearchBadge("!") state.value = SearchUiState.Error } } @@ -65,7 +76,7 @@ class SearchViewModel( override fun finishInstall(id: Int) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { val updates = state.value.mutableUpdates().removeId(id) state.value = SearchUiState.Success(updates) - mainViewModel.changeSearchBadge(updates.size.toString()) + badger.changeSearchBadge(updates.size.toString()) installer.finish() } diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt index adaed72b..3f0be9d4 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt @@ -9,6 +9,7 @@ import com.apkupdater.prefs.Prefs import com.apkupdater.repository.AppsRepository import com.apkupdater.ui.theme.isDarkTheme import com.apkupdater.util.Clipboard +import com.apkupdater.util.Themer import com.apkupdater.util.UpdatesNotification import com.apkupdater.worker.UpdatesWorker import com.google.gson.Gson @@ -21,13 +22,13 @@ import kotlinx.coroutines.launch class SettingsViewModel( - private val mainViewModel: MainViewModel, private val prefs: Prefs, private val notification: UpdatesNotification, private val workManager: WorkManager, private val clipboard: Clipboard, private val appsRepository: AppsRepository, - private val gson: Gson = GsonBuilder().setPrettyPrinting().create() + private val gson: Gson = GsonBuilder().setPrettyPrinting().create(), + private val themer: Themer ) : ViewModel() { val state = MutableStateFlow(SettingsUiState.Settings) @@ -72,7 +73,7 @@ class SettingsViewModel( fun setTheme(theme: Int) { prefs.theme.put(theme) - mainViewModel.setTheme(isDarkTheme(theme)) + themer.setTheme(isDarkTheme(theme)) } fun setRootInstall(b: Boolean) { @@ -119,7 +120,7 @@ class SettingsViewModel( } } - fun copyAppLogs() =viewModelScope.launch(Dispatchers.IO) { + fun copyAppLogs() = viewModelScope.launch(Dispatchers.IO) { val process = Runtime.getRuntime().exec("logcat -d") val data = process.inputStream.readBytes() clipboard.copy(data.decodeToString(), "App Logs") diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/UpdatesViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/UpdatesViewModel.kt index 0c361099..909c7e59 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/UpdatesViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/UpdatesViewModel.kt @@ -5,10 +5,15 @@ import com.apkupdater.data.ui.AppUpdate import com.apkupdater.data.ui.UpdatesUiState import com.apkupdater.data.ui.removeId import com.apkupdater.data.ui.setIsInstalling +import com.apkupdater.data.ui.setProgress import com.apkupdater.prefs.Prefs import com.apkupdater.repository.UpdatesRepository +import com.apkupdater.util.Badger import com.apkupdater.util.Downloader +import com.apkupdater.util.InstallLog import com.apkupdater.util.SessionInstaller +import com.apkupdater.util.SnackBar +import com.apkupdater.util.Stringer import com.apkupdater.util.launchWithMutex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -18,25 +23,33 @@ import kotlinx.coroutines.sync.Mutex class UpdatesViewModel( - private val mainViewModel: MainViewModel, private val updatesRepository: UpdatesRepository, private val installer: SessionInstaller, private val prefs: Prefs, - downloader: Downloader -) : InstallViewModel(mainViewModel, downloader, installer, prefs) { + private val badger: Badger, + downloader: Downloader, + snackBar: SnackBar, + stringer: Stringer, + installLog: InstallLog +) : InstallViewModel(downloader, installer, prefs, snackBar, stringer, installLog) { private val mutex = Mutex() private val state = MutableStateFlow(UpdatesUiState.Loading) init { - subscribeToInstallLog { log -> sendInstallSnack(state.value.updates(), log) } + subscribeToInstallStatus { status -> + sendInstallSnack(state.value.updates(), status) + } + subscribeToInstallProgress { progress -> + state.value = UpdatesUiState.Success(state.value.mutableUpdates().setProgress(progress)) + } } fun state(): StateFlow = state fun refresh(load: Boolean = true) = viewModelScope.launchWithMutex(mutex, Dispatchers.IO) { if (load) state.value = UpdatesUiState.Loading - mainViewModel.changeUpdatesBadge("") + badger.changeUpdatesBadge("") updatesRepository.updates().collect { setSuccess(it) } @@ -78,7 +91,7 @@ class UpdatesViewModel( .filterIgnoredVersions(prefs.ignoredVersions.get()) .let { state.value = UpdatesUiState.Success(it) - mainViewModel.changeUpdatesBadge(it.size.toString()) + badger.changeUpdatesBadge(it.size.toString()) } } diff --git a/app/src/main/kotlin/com/apkupdater/worker/UpdatesWorker.kt b/app/src/main/kotlin/com/apkupdater/worker/UpdatesWorker.kt index 14800c37..561c2404 100644 --- a/app/src/main/kotlin/com/apkupdater/worker/UpdatesWorker.kt +++ b/app/src/main/kotlin/com/apkupdater/worker/UpdatesWorker.kt @@ -36,7 +36,10 @@ class UpdatesWorker( workManager.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request) } - private fun randomDelay() = Random.nextLong(0, 59 * 60 * 1_000) + private fun randomDelay() = if (prefs.useApkMirror.get()) + Random.nextLong(0, 59 * 60 * 1_000) + else + Random.nextLong(-5 * 60 * 1_000, 5 * 60 * 1_000) private fun getDays() = when(prefs.alarmFrequency.get()) { 0 -> 1L