feat(update): notify about app updates on launch (chat banner) (#891)
Fires one silent update check per installed version at app start (UpdateNoticeManager, release builds only) and shows a non-blocking banner on the chat screen when a newer release exists. The About tab adopts the launch result instead of re-pinging GitHub; dismissals return on the next launch via the persisted latest tag. Closes #890 Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
ff8370bb8d
commit
daf33dcb4c
14 files changed
+535
-31
No files matched your search
@@ -77,8 +77,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.HistoryScreen
|
||||
import com.m57.hermescontrol.NavigationController
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.SettingsAbout
|
||||
import com.m57.hermescontrol.data.model.Attachment
|
||||
import com.m57.hermescontrol.data.model.AttachmentSource
|
||||
import com.m57.hermescontrol.data.update.AppUpdateCache
|
||||
import com.m57.hermescontrol.data.update.AppUpdateState
|
||||
import com.m57.hermescontrol.data.update.UpdateNoticeManager
|
||||
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
@@ -102,6 +106,7 @@ import com.m57.hermescontrol.ui.common.AutoScrollingTitleText
|
||||
import com.m57.hermescontrol.ui.common.CredentialWarningBanner
|
||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.UpdateNoticeBanner
|
||||
import com.m57.hermescontrol.ui.model.components.ModelPickerDialog
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -518,6 +523,25 @@ fun ChatScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Issue #890: launch update check — non-blocking banner when a
|
||||
// newer release exists. "Update" jumps to the About-tab install
|
||||
// flow; "Later" dismisses for the session (it returns next launch
|
||||
// via the persisted latest tag). Release-only builds
|
||||
// (UpdateNoticeManager.enabled = !BuildConfig.DEBUG).
|
||||
val updateNotice by AppUpdateCache.state.collectAsStateWithLifecycle()
|
||||
if (UpdateNoticeManager.enabled && !AppUpdateCache.dismissed) {
|
||||
val noticeTag =
|
||||
(updateNotice as? AppUpdateState.UpdateAvailable)?.latestTag
|
||||
?: UpdateNoticeManager.noticeTag()
|
||||
if (noticeTag != null) {
|
||||
UpdateNoticeBanner(
|
||||
latestTag = noticeTag,
|
||||
onUpdate = { NavigationController.navigateTo(SettingsAbout) },
|
||||
onDismiss = { AppUpdateCache.dismiss() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = searchState.isActive,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.m57.hermescontrol.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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 com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Chat-screen banner for the launch update check (issue #890): a newer
|
||||
* Hermes Mobile release exists. "Update" jumps into the About-tab install
|
||||
* flow; "Later" dismisses for the session (the banner returns next launch).
|
||||
*/
|
||||
@Composable
|
||||
fun UpdateNoticeBanner(
|
||||
latestTag: String,
|
||||
onUpdate: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Update,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.update_notice_banner_text, latestTag),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
TextButton(onClick = onUpdate) {
|
||||
Text(
|
||||
text = stringResource(R.string.update_notice_banner_action),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_dismiss),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,11 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.BuildConfig
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.update.AppUpdateCache
|
||||
import com.m57.hermescontrol.data.update.AppUpdateChecker
|
||||
import com.m57.hermescontrol.data.update.AppUpdateState
|
||||
import com.m57.hermescontrol.data.update.isNewerVersion
|
||||
import com.m57.hermescontrol.data.update.releaseTag
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -19,36 +22,6 @@ import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/** UI state of the About-tab in-app updater (issue #867). */
|
||||
sealed interface AppUpdateState {
|
||||
/** Nothing shown yet (no check run in this session). */
|
||||
data object Idle : AppUpdateState
|
||||
|
||||
/** A check is in flight. */
|
||||
data object Checking : AppUpdateState
|
||||
|
||||
/** Latest release equals the installed version. */
|
||||
data class UpToDate(val latestTag: String) : AppUpdateState
|
||||
|
||||
/** A newer release with an APK asset exists. */
|
||||
data class UpdateAvailable(
|
||||
val latestTag: String,
|
||||
val apkUrl: String,
|
||||
val sizeBytes: Long,
|
||||
) : AppUpdateState
|
||||
|
||||
/** APK download in progress; [progress] is 0..1. */
|
||||
data class Downloading(val progress: Float) : AppUpdateState
|
||||
|
||||
/** Download finished and the system package installer was launched. */
|
||||
data class Installing(val latestTag: String) : AppUpdateState
|
||||
|
||||
/** Install-from-unknown-sources not granted for this app yet. */
|
||||
data object NeedsUnknownSourcesPermission : AppUpdateState
|
||||
|
||||
data class Error(val message: String) : AppUpdateState
|
||||
}
|
||||
|
||||
/**
|
||||
* Drives the in-app self-update flow (issue #867): check the GitHub
|
||||
* releases API, download the release APK, and hand it to the system package
|
||||
@@ -76,6 +49,14 @@ class AppUpdateViewModel(
|
||||
// on every visit.
|
||||
if (AuthManager.getUpdateCheckDoneForVersion() != currentVersion) {
|
||||
checkForUpdate()
|
||||
} else {
|
||||
// Issue #890: the launch check (UpdateNoticeManager) already ran
|
||||
// for this version — adopt its result instead of pinging GitHub
|
||||
// again, so the About tab agrees with the chat banner.
|
||||
val cached = AppUpdateCache.state.value
|
||||
if (cached is AppUpdateState.UpdateAvailable || cached is AppUpdateState.UpToDate) {
|
||||
_state.value = cached
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +96,9 @@ class AppUpdateViewModel(
|
||||
} else {
|
||||
AppUpdateState.UpToDate(latestTag = info.tagName)
|
||||
}
|
||||
// Keep the launch notice (issue #890) in sync with manual checks.
|
||||
AppUpdateCache.update(_state.value)
|
||||
_state.value.releaseTag()?.let { AuthManager.setLastKnownLatestTag(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.BuildConfig
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.ui.settings.AppUpdateState
|
||||
import com.m57.hermescontrol.data.update.AppUpdateState
|
||||
import com.m57.hermescontrol.ui.settings.InfoRow
|
||||
import com.m57.hermescontrol.ui.settings.SectionCard
|
||||
|
||||
|
||||
Reference in new issue
Block a user