diff --git a/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt b/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt index e89478a..72a100f 100644 --- a/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt +++ b/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt @@ -288,6 +288,7 @@ fun InfoApp() { .consumeWindowInsets(innerPadding), entries = releasesUiState.value.entries.toSortedMap().toList().asReversed(), + releaseStates = releasesUiState.value.releaseStates.toSortedMap().toList(), updateChangelog = { useCaches, onFinishedUpdating -> releasesViewModel.updateChangelog( useCaches = useCaches, @@ -304,6 +305,15 @@ fun InfoApp() { onFinishedUpdating = onFinishedUpdating, ) }, + updateReleaseStates = { useCaches, onFinishedUpdating -> + releasesViewModel.updateReleaseStates( + useCaches = useCaches, + showSnackbarError = { + snackbarHostState.showSnackbar(it) + }, + onFinishedUpdating = onFinishedUpdating, + ) + }, changelogLazyListState = changelogLazyListState, additionalContentPadding = PaddingValues( start = innerPadding.calculateStartPadding(layoutDirection), diff --git a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleaseState.kt b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleaseState.kt new file mode 100644 index 0000000..0837967 --- /dev/null +++ b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleaseState.kt @@ -0,0 +1,50 @@ +package app.grapheneos.info.ui.releases + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.grapheneos.info.R + +@Composable +fun ReleaseState(releaseStates: List>) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + val releasePhases = mapOf( + "stable" to R.string.stable, + "beta" to R.string.beta, + "alpha" to R.string.alpha, + ) + for ((releasePhase, resourceId) in releasePhases) { + Column ( + modifier = Modifier.weight(1f), + ) { + ElevatedCard ( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = resourceId), + style = typography.titleMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 5.dp).align(Alignment.CenterHorizontally) + ) + Text( + text = releaseStates.find { it.first == releasePhase }?.second.toString(), + style = typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 5.dp).align(Alignment.CenterHorizontally) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesScreen.kt b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesScreen.kt index 5805453..c833e7d 100644 --- a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesScreen.kt +++ b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesScreen.kt @@ -37,7 +37,9 @@ fun ReleasesScreen( modifier: Modifier = Modifier, showSnackbarError: (String) -> Unit, entries: List>, + releaseStates: List>, updateChangelog: (useCaches: Boolean, finishedUpdating: () -> Unit) -> Unit, + updateReleaseStates: (useCaches: Boolean, finishedUpdating: () -> Unit) -> Unit, changelogLazyListState: LazyListState, additionalContentPadding: PaddingValues = PaddingValues(0.dp) ) { @@ -55,6 +57,7 @@ fun ReleasesScreen( if (event == Lifecycle.Event.ON_START) { refreshCoroutineScope.launch { updateChangelog(true) {} + updateReleaseStates(true) {} } } } @@ -66,19 +69,32 @@ fun ReleasesScreen( } } - var isRefreshing by rememberSaveable { mutableStateOf(false) } + var isChangelogRefreshing by rememberSaveable { mutableStateOf(false) } + var isReleaseStatesRefreshing by rememberSaveable { mutableStateOf(false) } val state = rememberPullToRefreshState() PullToRefreshBox( - isRefreshing = isRefreshing, + isRefreshing = isChangelogRefreshing || isReleaseStatesRefreshing, onRefresh = { - isRefreshing = true + isChangelogRefreshing = true + isReleaseStatesRefreshing = true updateChangelog(false) { - isRefreshing = false + isChangelogRefreshing = false - refreshCoroutineScope.launch { - state.animateToHidden() + if (!isReleaseStatesRefreshing) { + refreshCoroutineScope.launch { + state.animateToHidden() + } + } + } + updateReleaseStates(false) { + isReleaseStatesRefreshing = false + + if (!isChangelogRefreshing) { + refreshCoroutineScope.launch { + state.animateToHidden() + } } } }, @@ -93,6 +109,15 @@ fun ReleasesScreen( additionalContentPadding = additionalContentPadding, verticalArrangement = Arrangement.Top ) { + item { + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + ReleaseState(releaseStates) + } + } items( items = entries, key = { it.first }) { diff --git a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesUiState.kt b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesUiState.kt index ff32841..20faa2e 100644 --- a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesUiState.kt +++ b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesUiState.kt @@ -13,4 +13,7 @@ class ReleasesUiState(savedStateHandle: SavedStateHandle) { } /** Unsorted release notes, use .toSortedMap().toList().asReversible() to get them in the proper order. */ val entries: MutableMap = mutableStateMapOf() + + /** Unsorted release states, use .toSortedMap().toList().asReversible() to get them in the proper order. */ + val releaseStates: MutableMap = mutableStateMapOf("stable" to "-", "beta" to "-", "alpha" to "-") } \ No newline at end of file diff --git a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesViewModel.kt b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesViewModel.kt index dfa6647..e6a0590 100644 --- a/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesViewModel.kt +++ b/app/src/main/kotlin/app/grapheneos/info/ui/releases/ReleasesViewModel.kt @@ -38,6 +38,11 @@ class ReleasesViewModel( countAsInitialScroll = false, onFinishedUpdating = {}, ) + updateReleaseStates( + useCaches = true, + showSnackbarError = {}, + onFinishedUpdating = {}, + ) } fun updateChangelog( @@ -151,4 +156,74 @@ class ReleasesViewModel( } } } -} + + fun updateReleaseStates( + useCaches: Boolean, + showSnackbarError: suspend (message: String) -> Unit, + onFinishedUpdating: () -> Unit = {}, + ) { + var board = android.os.Build.BOARD + val releasePhases = arrayOf("stable", "beta", "alpha") + for (releasePhase in releasePhases) { + viewModelScope.launch(Dispatchers.IO) { + try { + val url = URL("https://releases.grapheneos.org/$board-$releasePhase") + val connection = url.openConnection() as HttpsURLConnection + + connection.apply { + sslSocketFactory = tlsSocketFactory + connectTimeout = 10_000 + readTimeout = 30_000 + } + + try { + connection.useCaches = useCaches + + connection.connect() + + val responseText = String(connection.inputStream.readBytes()) + Log.e(TAG, responseText); + + withContext(Dispatchers.Main) { + _uiState.value.releaseStates[releasePhase] = responseText.split(" ")[0] + } + + connection.disconnect() + } catch (e: SocketTimeoutException) { + val errorMessage = + application.getString(R.string.update_release_states_socket_timeout_exception_snackbar_message) + Log.e(TAG, errorMessage, e) + viewModelScope.launch { + showSnackbarError("$errorMessage: $e") + } + } catch (e: IOException) { + val errorMessage = + application.getString(R.string.update_release_states_io_exception_snackbar_message) + Log.e(TAG, errorMessage, e) + viewModelScope.launch { + showSnackbarError("$errorMessage: $e") + } + } catch (e: UnknownServiceException) { + val errorMessage = + application.getString(R.string.update_release_states_unknown_service_exception_snackbar_message) + Log.e(TAG, errorMessage, e) + viewModelScope.launch { + showSnackbarError("$errorMessage: $e") + } + } finally { + connection.disconnect() + } + } catch (e: IOException) { + val errorMessage = + application.getString(R.string.update_release_states_failed_to_create_httpsurlconnection_snackbar_message) + Log.e(TAG, errorMessage, e) + viewModelScope.launch { + showSnackbarError("$errorMessage: $e") + } + } finally { + onFinishedUpdating() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cdc937e..52e7e28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -143,4 +143,13 @@ our Wise Quick Pay link " rather than using the bank account details. This enables sending donations from anywhere Wise is available. If the currency you're using isn't listed due to not being one of the currencies where we accept local bank transfers, please select CAD or USD so we can avoid another conversion." Wise + Stable + Beta + Alpha + Socket Timeout Exception + Failed to retrieve latest release states + Unknown Service Exception + Failed to create + HttpsURLConnection +