diff --git a/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt b/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt index 96f42ba..e2f18c8 100644 --- a/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt +++ b/app/src/main/kotlin/app/grapheneos/info/InfoApp.kt @@ -286,6 +286,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, @@ -302,6 +303,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..0836a00 --- /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) + ) + } + } + } + } +} 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 7051985..aa5033c 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( @@ -136,4 +141,74 @@ class ReleasesViewModel( } } } + + fun updateReleaseStates( + useCaches: Boolean, + showSnackbarError: suspend (message: String) -> Unit, + onFinishedUpdating: () -> Unit = {}, + ) { + val board = android.os.Build.DEVICE + 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() + } + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb4e569..5bf5fdd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,4 +139,13 @@ Unable to open link. Make sure a browser is installed and enabled on your device. Info about the releases See all release notes + Stable + Beta + Alpha + Socket Timeout Exception + Failed to retrieve latest release states + Unknown Service Exception + Failed to create + HttpsURLConnection +