Implement built in updater (#6)

* implement release check api

* add options for updater

* fix settings scroll

* minor preference reorganization

* implement initial release properties

* add downloading code

* change download api

* add extraction code

* fix modifier padding

* update geode status after update

* set stored release after update

* disable updater by default for armv8

* add settings toasts

* use snackbar instead of toast

* small cleanup

* remove unused constant

* implement stable channel

* child doesn't require viewmodel

* update release format

* enable updater by default for all archs

* modify updater to be global

* fix copyfile reference

* correct state ordering issue

* only allow one update at a time

* show message in settings on second update

* allow update cancellation and retry

* disable start until update finished

* fix countdown lifecycle

* always sync release state to main activity

* allow scrolling main activity

* use a library for internal downloads
mat allowed me to

* show download progress in settings

* no internet messages

* enable api request cache

* delete old library before extracting

* oops....
This commit is contained in:
Chloe authored and GitHub committed 2024-01-06 15:20:21 -07:00
1 parent c421e37e1a
commit 4c555881b5
17 files changed
+1184 -209

No files matched your search

@@ -9,26 +9,36 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
import java.net.ConnectException
import java.net.UnknownHostException
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -78,12 +88,89 @@ fun onOpenFolder(context: Context) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
fun UpdateIndicator(
snackbarHostState: SnackbarHostState,
updateStatus: ReleaseViewModel.ReleaseUIState
) {
val context = LocalContext.current
var enablePopup by remember { mutableStateOf(false) }
when (updateStatus) {
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
enablePopup = true
CircularProgressIndicator()
}
is ReleaseViewModel.ReleaseUIState.InDownload -> {
// is this the ideal design? idk
enablePopup = true
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
CircularProgressIndicator(progress.toFloat())
}
else -> {}
}
LaunchedEffect(updateStatus) {
// only show popup if some progress was shown
if (!enablePopup) {
return@LaunchedEffect
}
when (updateStatus) {
is ReleaseViewModel.ReleaseUIState.Failure -> {
val message = when (updateStatus.exception) {
is UnknownHostException, is ConnectException ->
context.getString(R.string.release_fetch_no_internet)
else -> context.getString(R.string.preference_check_for_updates_failed)
}
snackbarHostState.showSnackbar(message)
}
is ReleaseViewModel.ReleaseUIState.Finished -> {
if (updateStatus.hasUpdated) {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_success)
)
} else {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_none_found)
)
}
}
else -> {}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val context = LocalContext.current
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
val updateStatus by releaseViewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
var showUpdateInProgress by remember { mutableStateOf(false) }
if (showUpdateInProgress) {
LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_already_updating)
)
showUpdateInProgress = false
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
topBar = {
TopAppBar(
navigationIcon = {
@@ -107,18 +194,19 @@ fun SettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
Column(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OptionsGroup(context.getString(R.string.preference_category_testing)) {
SettingsCard(
title = context.getString(R.string.preference_load_testing_name),
preferenceId = R.string.preference_load_testing
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
)
SettingsCard(
title = context.getString(R.string.preference_load_automatically_name),
description = context.getString(R.string.preference_load_automatically_description),
preferenceId = R.string.preference_load_automatically
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
)
OptionsButton(
title = context.getString(R.string.preferences_copy_external_button),
@@ -126,6 +214,41 @@ fun SettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
onClick = { onOpenFolder(context) }
)
}
OptionsGroup(context.getString(R.string.preference_category_updater)) {
SettingsCard(
title = context.getString(R.string.preference_update_automatically_name),
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
)
SettingsCard(
title = context.getString(R.string.preference_release_channel_name),
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL,
)
OptionsCard(
title = {
OptionsTitle(
title = stringResource(R.string.preference_check_for_updates_button),
description = stringResource(
R.string.preference_check_for_updates_description,
currentRelease ?: "unknown"
)
)
},
modifier = Modifier
.clickable(
onClick = {
if (releaseViewModel.isInUpdate) {
showUpdateInProgress = true
} else {
releaseViewModel.runReleaseCheck()
}
},
role = Role.Button
)
) {
UpdateIndicator(snackbarHostState, updateStatus)
}
}
/*
OptionsGroup("Data") {
OptionsButton(
@@ -139,28 +262,16 @@ fun SettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
)
}
fun toggleSetting(context: Context, @StringRes preferenceId: Int): Boolean {
val preferences = context.getSharedPreferences(
context.getString(R.string.preference_file_key), Context.MODE_PRIVATE
)
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
val current = preferences.getBoolean(
context.getString(preferenceId), false
)
preferences.edit {
putBoolean(context.getString(preferenceId), !current)
commit()
}
return preferences.getBoolean(context.getString(preferenceId), false)
return preferences.toggleBoolean(preferenceKey)
}
fun getSetting(context: Context, @StringRes preferenceId: Int): Boolean {
val preferences = context.getSharedPreferences(
context.getString(R.string.preference_file_key), Context.MODE_PRIVATE
)
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
val preferences = PreferenceUtils.get(context)
return preferences.getBoolean(context.getString(preferenceId), false)
return preferences.getBoolean(preferenceKey)
}
@Composable
@@ -190,10 +301,11 @@ fun OptionsButton(title: String, description: String? = null, onClick: () -> Uni
}
@Composable
fun SettingsCard(title: String, description: String? = null, @StringRes preferenceId: Int) {
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
val settingEnabled = remember {
mutableStateOf(getSetting(context, preferenceId)) }
mutableStateOf(getSetting(context, preferenceKey))
}
OptionsCard(
title = {
@@ -205,7 +317,7 @@ fun SettingsCard(title: String, description: String? = null, @StringRes preferen
},
modifier = Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceId) },
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
)
) {
@@ -253,11 +365,11 @@ fun OptionsCardPreview() {
SettingsCard(
title = "Load files from /test",
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
preferenceId = R.string.preference_load_testing
preferenceKey = PreferenceUtils.Key.LOAD_TESTING
)
SettingsCard(
title = "Testing option 2",
preferenceId = R.string.preference_load_automatically
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
}
}