feat: check for updates on startup (#1462)

This commit is contained in:
Robert 2023-11-05 12:19:55 +00:00 committed by GitHub
parent 1a83315424
commit 1dc41badd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 78 deletions

View File

@ -1,27 +1,25 @@
package app.revanced.manager package app.revanced.manager
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.res.stringResource
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination 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.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen 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.InstallerScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen 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.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel 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.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController import dev.olshevski.navigation.reimagined.rememberNavController
import org.koin.core.parameter.parametersOf
import org.koin.androidx.compose.getViewModel as getComposeViewModel import org.koin.androidx.compose.getViewModel as getComposeViewModel
import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getAndroidViewModel
import org.koin.core.parameter.parametersOf
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ -51,6 +47,8 @@ class MainActivity : ComponentActivity() {
val vm: MainViewModel = getAndroidViewModel() val vm: MainViewModel = getAndroidViewModel()
vm.importLegacySettings(this)
setContent { setContent {
val theme by vm.prefs.theme.getAsState() val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState() val dynamicColor by vm.prefs.dynamicColor.getAsState()
@ -66,46 +64,30 @@ class MainActivity : ComponentActivity() {
val firstLaunch by vm.prefs.firstLaunch.getAsState() val firstLaunch by vm.prefs.firstLaunch.getAsState()
if (firstLaunch) { if (firstLaunch) AutoUpdatesDialog(vm::applyAutoUpdatePrefs)
var legacyActivityState by rememberSaveable { mutableStateOf(LegacyActivity.NOT_LAUNCHED) }
if (legacyActivityState == LegacyActivity.NOT_LAUNCHED) { vm.updatedManagerVersion?.let {
val launcher = rememberLauncherForActivityResult( AlertDialog(
contract = ActivityResultContracts.StartActivityForResult() onDismissRequest = vm::dismissUpdateDialog,
) { result: ActivityResult -> confirmButton = {
if (result.resultCode == RESULT_OK) { TextButton(
if (result.data != null) { onClick = {
val jsonData = result.data!!.getStringExtra("data")!! vm.dismissUpdateDialog()
vm.applyLegacySettings(jsonData) navController.navigate(Destination.Settings(SettingsDestination.UpdateProgress))
} }
} else { ) {
legacyActivityState = LegacyActivity.FAILED Text(stringResource(R.string.update))
toast(getString(R.string.legacy_import_failed))
} }
} },
dismissButton = {
val intent = Intent().apply { TextButton(onClick = vm::dismissUpdateDialog) {
setClassName( Text(stringResource(R.string.dismiss_temporary))
"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
} }
} },
icon = { Icon(Icons.Outlined.Update, null) },
legacyActivityState = LegacyActivity.LAUNCHED title = { Text(stringResource(R.string.update_available)) },
} else if (legacyActivityState == LegacyActivity.FAILED) { text = { Text(stringResource(R.string.update_available_description, it)) }
AutoUpdatesDialog(vm::applyAutoUpdatePrefs) )
}
} }
AnimatedNavHost( AnimatedNavHost(
@ -113,7 +95,7 @@ class MainActivity : ComponentActivity() {
) { destination -> ) { destination ->
when (destination) { when (destination) {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) }, onSettingsClick = { navController.navigate(Destination.Settings()) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) }, onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onAppClick = { installedApp -> onAppClick = { installedApp ->
navController.navigate( navController.navigate(
@ -138,7 +120,8 @@ class MainActivity : ComponentActivity() {
) )
is Destination.Settings -> SettingsScreen( is Destination.Settings -> SettingsScreen(
onBackClick = { navController.pop() } onBackClick = { navController.pop() },
startDestination = destination.startDestination
) )
is Destination.AppSelector -> AppSelectorScreen( is Destination.AppSelector -> AppSelectorScreen(
@ -199,10 +182,4 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
private enum class LegacyActivity {
NOT_LAUNCHED,
LAUNCHED,
FAILED
}
} }

View File

@ -11,16 +11,16 @@ import kotlinx.parcelize.RawValue
sealed interface Destination : Parcelable { sealed interface Destination : Parcelable {
@Parcelize @Parcelize
object Dashboard : Destination data object Dashboard : Destination
@Parcelize @Parcelize
data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination data class InstalledApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize @Parcelize
object AppSelector : Destination data object AppSelector : Destination
@Parcelize @Parcelize
object Settings : Destination data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
@Parcelize @Parcelize
data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination

View File

@ -43,10 +43,16 @@ import app.revanced.manager.ui.component.settings.SettingsListItem
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
startDestination: SettingsDestination,
viewModel: SettingsViewModel = getViewModel() viewModel: SettingsViewModel = getViewModel()
) { ) {
val navController = val navController = rememberNavController(startDestination)
rememberNavController<SettingsDestination>(startDestination = SettingsDestination.Settings)
val backClick: () -> Unit = {
if (navController.backstack.entries.size == 1)
onBackClick()
else navController.pop()
}
val context = LocalContext.current val context = LocalContext.current
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
@ -92,48 +98,48 @@ fun SettingsScreen(
when (destination) { when (destination) {
is SettingsDestination.General -> GeneralSettingsScreen( is SettingsDestination.General -> GeneralSettingsScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
viewModel = viewModel viewModel = viewModel
) )
is SettingsDestination.Advanced -> AdvancedSettingsScreen( is SettingsDestination.Advanced -> AdvancedSettingsScreen(
onBackClick = { navController.pop() } onBackClick = backClick
) )
is SettingsDestination.Updates -> UpdatesSettingsScreen( is SettingsDestination.Updates -> UpdatesSettingsScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) }, onChangelogClick = { navController.navigate(SettingsDestination.Changelogs) },
onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) } onUpdateClick = { navController.navigate(SettingsDestination.UpdateProgress) }
) )
is SettingsDestination.Downloads -> DownloadsSettingsScreen( is SettingsDestination.Downloads -> DownloadsSettingsScreen(
onBackClick = { navController.pop() } onBackClick = backClick
) )
is SettingsDestination.ImportExport -> ImportExportSettingsScreen( is SettingsDestination.ImportExport -> ImportExportSettingsScreen(
onBackClick = { navController.pop() } onBackClick = backClick
) )
is SettingsDestination.About -> AboutSettingsScreen( is SettingsDestination.About -> AboutSettingsScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) }, onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) } onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }
) )
is SettingsDestination.UpdateProgress -> UpdateProgressScreen( is SettingsDestination.UpdateProgress -> UpdateProgressScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
) )
is SettingsDestination.Changelogs -> ChangelogsScreen( is SettingsDestination.Changelogs -> ChangelogsScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
) )
is SettingsDestination.Contributors -> ContributorScreen( is SettingsDestination.Contributors -> ContributorScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
) )
is SettingsDestination.Licenses -> LicensesScreen( is SettingsDestination.Licenses -> LicensesScreen(
onBackClick = { navController.pop() }, onBackClick = backClick,
) )
is SettingsDestination.Settings -> { is SettingsDestination.Settings -> {
@ -141,7 +147,7 @@ fun SettingsScreen(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.settings), title = stringResource(R.string.settings),
onBackClick = onBackClick, onBackClick = backClick,
) )
} }
) { paddingValues -> ) { paddingValues ->

View File

@ -1,15 +1,32 @@
package app.revanced.manager.ui.viewmodel 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.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.ViewModel
import androidx.lifecycle.viewModelScope 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.bundles.PatchBundleSource.Companion.asRemoteOrNull
import app.revanced.manager.domain.manager.KeystoreManager import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SerializedSelection 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.ui.theme.Theme
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -19,12 +36,42 @@ class MainViewModel(
private val patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository,
private val patchSelectionRepository: PatchSelectionRepository, private val patchSelectionRepository: PatchSelectionRepository,
private val keystoreManager: KeystoreManager, private val keystoreManager: KeystoreManager,
private val reVancedAPI: ReVancedAPI,
private val app: Application,
private val networkInfo: NetworkInfo,
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : 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 { fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch {
prefs.firstLaunch.update(false) prefs.firstLaunch.update(false)
prefs.managerAutoUpdates.update(manager) prefs.managerAutoUpdates.update(manager)
if (manager) checkForManagerUpdates()
if (patches) { if (patches) {
with(patchBundleRepository) { with(patchBundleRepository) {
sources 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 json = Json { ignoreUnknownKeys = true }
val settings = json.decodeFromString<LegacySettings>(data) val settings = json.decodeFromString<LegacySettings>(data)
@ -48,7 +127,7 @@ class MainViewModel(
1 to Theme.LIGHT, 1 to Theme.LIGHT,
2 to Theme.DARK 2 to Theme.DARK
) )
prefs.theme.update(themeMap[theme]!!) prefs.theme.update(themeMap[theme] ?: Theme.SYSTEM)
} }
settings.useDynamicTheme?.let { dynamicColor -> settings.useDynamicTheme?.let { dynamicColor ->
prefs.dynamicColor.update(dynamicColor) prefs.dynamicColor.update(dynamicColor)
@ -84,7 +163,6 @@ class MainViewModel(
settings.patches?.let { selection -> settings.patches?.let { selection ->
patchSelectionRepository.import(0, selection) patchSelectionRepository.import(0, selection)
} }
prefs.firstLaunch.update(false)
} }
@Serializable @Serializable

View File

@ -297,4 +297,9 @@
<string name="days_ago">%sd ago</string> <string name="days_ago">%sd ago</string>
<string name="invalid_date">Invalid date</string> <string name="invalid_date">Invalid date</string>
<string name="disable_battery_optimization">Disable battery optimization</string> <string name="disable_battery_optimization">Disable battery optimization</string>
<string name="failed_to_check_updates">Failed to check for updates</string>
<string name="dismiss_temporary">Not now</string>
<string name="update_available">New update available</string>
<string name="update_available_description">A new version (%s) is available for download.</string>
</resources> </resources>