From 1dc41badd9636362c72f0c1f9738c72aa926b286 Mon Sep 17 00:00:00 2001 From: Robert <72943079+CnC-Robert@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:19:55 +0000 Subject: [PATCH] feat: check for updates on startup (#1462) --- .../java/app/revanced/manager/MainActivity.kt | 95 +++++++------------ .../manager/ui/destination/Destination.kt | 6 +- .../manager/ui/screen/SettingsScreen.kt | 32 ++++--- .../manager/ui/viewmodel/MainViewModel.kt | 84 +++++++++++++++- app/src/main/res/values/strings.xml | 5 + 5 files changed, 144 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 4cc8e1c6..6aa1eee8 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -1,27 +1,25 @@ package app.revanced.manager -import android.content.ActivityNotFoundException -import android.content.Intent import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.destination.Destination -import app.revanced.manager.ui.screen.InstalledAppInfoScreen +import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.DashboardScreen +import app.revanced.manager.ui.screen.InstalledAppInfoScreen import app.revanced.manager.ui.screen.InstallerScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SettingsScreen @@ -30,17 +28,15 @@ import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel -import app.revanced.manager.util.tag -import app.revanced.manager.util.toast import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.rememberNavController +import org.koin.core.parameter.parametersOf import org.koin.androidx.compose.getViewModel as getComposeViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel -import org.koin.core.parameter.parametersOf class MainActivity : ComponentActivity() { @ExperimentalAnimationApi @@ -51,6 +47,8 @@ class MainActivity : ComponentActivity() { val vm: MainViewModel = getAndroidViewModel() + vm.importLegacySettings(this) + setContent { val theme by vm.prefs.theme.getAsState() val dynamicColor by vm.prefs.dynamicColor.getAsState() @@ -66,46 +64,30 @@ class MainActivity : ComponentActivity() { val firstLaunch by vm.prefs.firstLaunch.getAsState() - if (firstLaunch) { - var legacyActivityState by rememberSaveable { mutableStateOf(LegacyActivity.NOT_LAUNCHED) } - if (legacyActivityState == LegacyActivity.NOT_LAUNCHED) { - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult -> - if (result.resultCode == RESULT_OK) { - if (result.data != null) { - val jsonData = result.data!!.getStringExtra("data")!! - vm.applyLegacySettings(jsonData) + if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs) + + vm.updatedManagerVersion?.let { + AlertDialog( + onDismissRequest = vm::dismissUpdateDialog, + confirmButton = { + TextButton( + onClick = { + vm.dismissUpdateDialog() + navController.navigate(Destination.Settings(SettingsDestination.UpdateProgress)) } - } else { - legacyActivityState = LegacyActivity.FAILED - toast(getString(R.string.legacy_import_failed)) + ) { + Text(stringResource(R.string.update)) } - } - - val intent = Intent().apply { - setClassName( - "app.revanced.manager.flutter", - "app.revanced.manager.flutter.ExportSettingsActivity" - ) - } - - LaunchedEffect(Unit) { - try { - launcher.launch(intent) - } catch (e: Exception) { - if (e !is ActivityNotFoundException) { - toast(getString(R.string.legacy_import_failed)) - Log.e(tag, "Failed to launch legacy import activity: $e") - } - legacyActivityState = LegacyActivity.FAILED + }, + dismissButton = { + TextButton(onClick = vm::dismissUpdateDialog) { + Text(stringResource(R.string.dismiss_temporary)) } - } - - legacyActivityState = LegacyActivity.LAUNCHED - } else if (legacyActivityState == LegacyActivity.FAILED) { - AutoUpdatesDialog(vm::applyAutoUpdatePrefs) - } + }, + icon = { Icon(Icons.Outlined.Update, null) }, + title = { Text(stringResource(R.string.update_available)) }, + text = { Text(stringResource(R.string.update_available_description, it)) } + ) } AnimatedNavHost( @@ -113,7 +95,7 @@ class MainActivity : ComponentActivity() { ) { destination -> when (destination) { is Destination.Dashboard -> DashboardScreen( - onSettingsClick = { navController.navigate(Destination.Settings) }, + onSettingsClick = { navController.navigate(Destination.Settings()) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, onAppClick = { installedApp -> navController.navigate( @@ -138,7 +120,8 @@ class MainActivity : ComponentActivity() { ) is Destination.Settings -> SettingsScreen( - onBackClick = { navController.pop() } + onBackClick = { navController.pop() }, + startDestination = destination.startDestination ) is Destination.AppSelector -> AppSelectorScreen( @@ -199,10 +182,4 @@ class MainActivity : ComponentActivity() { } } } - - private enum class LegacyActivity { - NOT_LAUNCHED, - LAUNCHED, - FAILED - } } diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt index 6c01e783..a7712532 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt @@ -11,16 +11,16 @@ import kotlinx.parcelize.RawValue sealed interface Destination : Parcelable { @Parcelize - object Dashboard : Destination + data object Dashboard : Destination @Parcelize data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination @Parcelize - object AppSelector : Destination + data object AppSelector : Destination @Parcelize - object Settings : Destination + data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination @Parcelize data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index eb3464df..76bff712 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -43,10 +43,16 @@ import app.revanced.manager.ui.component.settings.SettingsListItem @Composable fun SettingsScreen( onBackClick: () -> Unit, + startDestination: SettingsDestination, viewModel: SettingsViewModel = getViewModel() ) { - val navController = - rememberNavController(startDestination = SettingsDestination.Settings) + val navController = rememberNavController(startDestination) + + val backClick: () -> Unit = { + if (navController.backstack.entries.size == 1) + onBackClick() + else navController.pop() + } val context = LocalContext.current val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager @@ -92,48 +98,48 @@ fun SettingsScreen( when (destination) { is SettingsDestination.General -> GeneralSettingsScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, viewModel = viewModel ) is SettingsDestination.Advanced -> AdvancedSettingsScreen( - onBackClick = { navController.pop() } + onBackClick = backClick ) is SettingsDestination.Updates -> UpdatesSettingsScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) }, onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) } ) is SettingsDestination.Downloads -> DownloadsSettingsScreen( - onBackClick = { navController.pop() } + onBackClick = backClick ) is SettingsDestination.ImportExport -> ImportExportSettingsScreen( - onBackClick = { navController.pop() } + onBackClick = backClick ) is SettingsDestination.About -> AboutSettingsScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, onContributorsClick = { navController.navigate(SettingsDestination.Contributors) }, onLicensesClick = { navController.navigate(SettingsDestination.Licenses) } ) is SettingsDestination.UpdateProgress -> UpdateProgressScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, ) is SettingsDestination.Changelogs -> ChangelogsScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, ) is SettingsDestination.Contributors -> ContributorScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, ) is SettingsDestination.Licenses -> LicensesScreen( - onBackClick = { navController.pop() }, + onBackClick = backClick, ) is SettingsDestination.Settings -> { @@ -141,7 +147,7 @@ fun SettingsScreen( topBar = { AppTopBar( title = stringResource(R.string.settings), - onBackClick = onBackClick, + onBackClick = backClick, ) } ) { paddingValues -> diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt index baf017b1..0fcfedfd 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -1,15 +1,32 @@ package app.revanced.manager.ui.viewmodel +import android.app.Application +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Build import android.util.Base64 +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.data.platform.NetworkInfo import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SerializedSelection +import app.revanced.manager.network.api.ReVancedAPI +import app.revanced.manager.network.utils.getOrThrow import app.revanced.manager.ui.theme.Theme +import app.revanced.manager.util.tag +import app.revanced.manager.util.toast import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -19,12 +36,42 @@ class MainViewModel( private val patchBundleRepository: PatchBundleRepository, private val patchSelectionRepository: PatchSelectionRepository, private val keystoreManager: KeystoreManager, + private val reVancedAPI: ReVancedAPI, + private val app: Application, + private val networkInfo: NetworkInfo, val prefs: PreferencesManager ) : ViewModel() { + var updatedManagerVersion: String? by mutableStateOf(null) + private set + + init { + viewModelScope.launch { checkForManagerUpdates() } + } + + fun dismissUpdateDialog() { + updatedManagerVersion = null + } + + private suspend fun checkForManagerUpdates() { + if (!prefs.managerAutoUpdates.get() || !networkInfo.isConnected()) return + + try { + reVancedAPI.getLatestRelease("revanced-manager").getOrThrow().let { release -> + updatedManagerVersion = release.metadata.tag.takeIf { it != Build.VERSION.RELEASE } + } + } catch (e: Exception) { + app.toast(app.getString(R.string.failed_to_check_updates)) + Log.e(tag, "Failed to check for updates", e) + } + } + fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch { prefs.firstLaunch.update(false) prefs.managerAutoUpdates.update(manager) + + if (manager) checkForManagerUpdates() + if (patches) { with(patchBundleRepository) { sources @@ -38,7 +85,39 @@ class MainViewModel( } } - fun applyLegacySettings(data: String) = viewModelScope.launch { + fun importLegacySettings(componentActivity: ComponentActivity) { + if (!prefs.firstLaunch.getBlocking()) return + + try { + val launcher = componentActivity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + if (result.resultCode == ComponentActivity.RESULT_OK) { + result.data?.getStringExtra("data")?.let { + applyLegacySettings(it) + } ?: app.toast(app.getString(R.string.legacy_import_failed)) + } else { + app.toast(app.getString(R.string.legacy_import_failed)) + } + } + + val intent = Intent().apply { + setClassName( + "app.revanced.manager.flutter", + "app.revanced.manager.flutter.ExportSettingsActivity" + ) + } + + launcher.launch(intent) + } catch (e: Exception) { + if (e !is ActivityNotFoundException) { + app.toast(app.getString(R.string.legacy_import_failed)) + Log.e(tag, "Failed to launch legacy import activity: $e") + } + } + } + + private fun applyLegacySettings(data: String) = viewModelScope.launch { val json = Json { ignoreUnknownKeys = true } val settings = json.decodeFromString(data) @@ -48,7 +127,7 @@ class MainViewModel( 1 to Theme.LIGHT, 2 to Theme.DARK ) - prefs.theme.update(themeMap[theme]!!) + prefs.theme.update(themeMap[theme] ?: Theme.SYSTEM) } settings.useDynamicTheme?.let { dynamicColor -> prefs.dynamicColor.update(dynamicColor) @@ -84,7 +163,6 @@ class MainViewModel( settings.patches?.let { selection -> patchSelectionRepository.import(0, selection) } - prefs.firstLaunch.update(false) } @Serializable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea55f13c..d4a07d1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -297,4 +297,9 @@ %sd ago Invalid date Disable battery optimization + + Failed to check for updates + Not now + New update available + A new version (%s) is available for download. \ No newline at end of file