feat(watch): sync indicator UI + POST_NOTIFICATIONS prompt

CaseListScreen shows a small ☁↑N at the top whenever the worker has
queue items; per-row ↑ next to the timestamp marks individual cases
that are still mid-upload. Both surface the SyncStateProvider directly
so the user sees the queue draining in real time.

MainActivity prompts for POST_NOTIFICATIONS on Wear OS 13+ — the
foreground service runs either way, but on API 33+ the persistent
notification only renders if the runtime grant is given. Result is
informational; we don't gate the recording flow on it (the doctor
still needs to be able to dictate even after a 'deny').
This commit is contained in:
2026-04-26 23:52:59 +02:00
parent 491b093b86
commit 68a77322d0
2 changed files with 42 additions and 4 deletions
@@ -37,6 +37,7 @@ fun CaseListScreen(
) {
val app = LocalContext.current.applicationContext as DoctateApp
val cases by app.caseStore.casesFlow.collectAsStateWithLifecycle()
val sync by app.syncStateProvider.state.collectAsStateWithLifecycle()
val listState = rememberScalingLazyListState()
// Display oldest-first so the newest entry sits next to the `Neu` EdgeButton.
@@ -62,6 +63,17 @@ fun CaseListScreen(
if (displayed.isEmpty()) {
EmptyState(onNewCase)
} else {
if (sync.queueLen > 0) {
Text(
text = "☁↑${sync.queueLen}",
style = MaterialTheme.typography.bodyExtraSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(top = 2.dp),
textAlign = TextAlign.Center,
)
}
ScalingLazyColumn(
state = listState,
contentPadding = contentPadding,
@@ -70,7 +82,11 @@ fun CaseListScreen(
modifier = Modifier.fillMaxSize(),
) {
items(displayed, key = { it.caseId }) { case ->
CaseRow(case, onTap = { onCaseTap(case.caseId) })
CaseRow(
case = case,
pendingUpload = case.caseId in sync.pendingCaseIds,
onTap = { onCaseTap(case.caseId) },
)
}
}
}
@@ -78,14 +94,14 @@ fun CaseListScreen(
}
@Composable
private fun CaseRow(case: CaseEntry, onTap: () -> Unit) {
private fun CaseRow(case: CaseEntry, pendingUpload: Boolean, onTap: () -> Unit) {
Card(
onClick = onTap,
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
) {
Text(
text = formatTime(case.createdAt),
text = if (pendingUpload) "${formatTime(case.createdAt)}" else formatTime(case.createdAt),
style = MaterialTheme.typography.bodyExtraSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterHorizontally),
@@ -1,11 +1,16 @@
package com.doctate.watch.presentation
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import kotlinx.coroutines.flow.MutableStateFlow
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.flow.MutableStateFlow
class MainActivity : ComponentActivity() {
private val seq = AtomicLong(0)
@@ -13,8 +18,14 @@ class MainActivity : ComponentActivity() {
NavCommand.Pending(0L, NavCommand.ShowList),
)
private val notificationsLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { /* result is informational; the foreground service still runs without it,
but the persistent notification will be hidden on Wear OS 13+. */ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestNotificationPermissionIfNeeded()
dispatch(intent)
setContent { AppNav(pendingCommand = pendingCommand) }
}
@@ -31,4 +42,15 @@ class MainActivity : ComponentActivity() {
cmd = NavCommand.fromIntent(intent),
)
}
private fun requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val granted = ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) == PackageManager.PERMISSION_GRANTED
if (!granted) {
notificationsLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}