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

View File

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

View File

@ -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<SettingsDestination>(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 ->

View File

@ -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<LegacySettings>(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

View File

@ -297,4 +297,9 @@
<string name="days_ago">%sd ago</string>
<string name="invalid_date">Invalid date</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>