Skip to content

[Q1 Quality] WearOS - Display last sync time and sync status#4969

Closed
sztomek wants to merge 16 commits intomainfrom
feat/wearos-refresh-improvements
Closed

[Q1 Quality] WearOS - Display last sync time and sync status#4969
sztomek wants to merge 16 commits intomainfrom
feat/wearos-refresh-improvements

Conversation

@sztomek
Copy link
Copy Markdown
Contributor

@sztomek sztomek commented Feb 9, 2026

Description

Now we display a refresh status indicator on the main screen. We also display the last refresh time under the Refresh now chip under Settings.

Fixes PCDROID-433

Testing Instructions

  1. Launch the app, log in
  2. You'll notice the refresh indicator on the main screen
  3. Navigate to Settings, scroll until you find the Refresh now chip
  4. Verify you see the last refresh time

Screenshots or Screencast

Screenshot_20260209_212010

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

@sztomek sztomek added this to the 8.6 milestone Feb 9, 2026
@sztomek sztomek requested a review from a team as a code owner February 9, 2026 20:23
@sztomek sztomek added the [Area] Wear OS watch app label Feb 9, 2026
@sztomek sztomek removed the request for review from a team February 9, 2026 20:23
@sztomek sztomek added the [Type] Enhancement Improve an existing feature. label Feb 9, 2026
@sztomek sztomek requested review from Copilot and geekygecko February 9, 2026 20:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds WearOS UI affordances for podcast refresh/sync by surfacing refresh-in-progress state on the main list and showing the last refresh time (or failure/never states) in Settings.

Changes:

  • Adds a “refreshing” header indicator on the main Watch list screen driven by Settings.refreshStateFlow.
  • Shows last refresh time / refresh status text under the “Refresh now” chip in Settings (with new absolute-time formatting utility).
  • Prevents redundant refresh triggers from Settings while a refresh is already in progress.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/settings/SettingsViewModel.kt Adds guard to avoid re-triggering refresh while already refreshing.
wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/settings/SettingsScreen.kt Displays refresh status text and formats last refresh time for Settings.
wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreenViewModel.kt Injects Settings and exposes refreshState in UI state via refreshStateFlow.
wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt Renders a rotating refresh icon + label header when refresh is in progress.
wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/TimeFormatUtil.kt New helper to format refresh timestamps for Wear settings (absolute time + “Just now”).
modules/services/localization/src/main/res/values/strings.xml Adds new localized strings for “Just now” and absolute “Last refresh: %s”.

Comment thread wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/WatchListScreen.kt Outdated
Copilot AI review requested due to automatic review settings February 11, 2026 20:27
@sztomek sztomek force-pushed the feat/wearos-refresh-improvements branch from 09e855a to 808e27c Compare February 11, 2026 20:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt Outdated
Copilot AI review requested due to automatic review settings February 11, 2026 21:18
@sztomek sztomek force-pushed the feat/wearos-refresh-improvements branch from ba95ebb to 231c232 Compare February 11, 2026 21:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

wear/src/test/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/podcast/PodcastViewModelTest.kt:83

  • These tests now run with a StandardTestDispatcher, but PodcastViewModel.uiState is a StateFlow with initialValue = UiState.Empty and the upstream collection runs on viewModelScope. Depending on coroutine scheduling, the first Turbine awaitItem() can be the initial Empty value, making this assertion flaky. Make the test deterministic by advancing the test scheduler before collecting, or by asserting the initial Empty emission and then awaiting the subsequent Loaded emission.
    fun `when podcast found with episodes, then emits Loaded state with episodes`() = runTest(coroutineRule.testDispatcher) {
        val podcast = createTestPodcast(uuid = testPodcastUuid)
        val episodes = listOf(
            createTestEpisode(uuid = "ep1", title = "Episode 1"),
            createTestEpisode(uuid = "ep2", title = "Episode 2"),

@sztomek sztomek force-pushed the feat/wearos-refresh-improvements branch from 231c232 to d9ac200 Compare February 11, 2026 21:31
Copilot AI review requested due to automatic review settings February 12, 2026 17:37
@sztomek sztomek force-pushed the feat/wearos-refresh-improvements branch from d9ac200 to 6d5f062 Compare February 12, 2026 17:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

@wpmobilebot wpmobilebot modified the milestones: 8.6, 8.7 Feb 16, 2026
Copilot AI review requested due to automatic review settings February 16, 2026 21:33
@dangermattic
Copy link
Copy Markdown
Collaborator

1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 8 comments.

Comment thread wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/MainActivity.kt Outdated
Copilot AI review requested due to automatic review settings February 17, 2026 11:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

curvedComposable {
Icon(
painter = painterResource(IR.drawable.ic_cloud_off),
contentDescription = "Offline",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contentDescription is hardcoded as "Offline" instead of using a localized string resource. For proper accessibility and internationalization, this should reference a string from the localization module (e.g., stringResource(LR.string.offline) or similar). This ensures screen readers announce the icon status in the user's language.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is TalkBack on Wear OS so this would be worth doing.

Copy link
Copy Markdown
Member

@geekygecko geekygecko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it's just me, but there's a crash happening. I can't run it on my Pixel 3 watch.

With that commented out the pull to refresh works well!

fun TimeTextWithConnectivity(
isConnected: Boolean,
modifier: Modifier = Modifier,
) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app is crashing on my Pixel 3 watch unless I comment out the following:

AppScaffold(
        // timeText = { TimeTextWithConnectivity(isConnected = isConnected) },
    ) {

The stack trace is a bit strange as it doesn't include any Pocket Casts code:

Process: au.com.shiftyjelly.pocketcasts.debug, PID: 6301
                                                                                                    java.lang.IllegalArgumentException: either height or width should be bounded
  	at androidx.wear.compose.foundation.CurvedLayoutKt$CurvedLayout$2$1.measure-3p2s80s(CurvedLayout.kt:121)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:699)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.MeasurePassDelegate.measure-BRTryo0(MeasurePassDelegate.kt:470)
  	at androidx.compose.foundation.layout.BoxMeasurePolicy.measure-3p2s80s(Box.kt:145)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:699)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.MeasurePassDelegate.measure-BRTryo0(MeasurePassDelegate.kt:470)
  	at androidx.wear.compose.foundation.CurvedComposableChild.initializeMeasure(CurvedComposable.kt:119)
  	at androidx.wear.compose.foundation.ContainerChild.initializeMeasure(CurvedContainer.kt:73)
  	at androidx.wear.compose.foundation.CurvedLayoutKt$CurvedLayout$2$1.measure-3p2s80s(CurvedLayout.kt:137)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:699)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.MeasurePassDelegate.measure-BRTryo0(MeasurePassDelegate.kt:470)
  	at androidx.compose.foundation.layout.BoxMeasurePolicy.measure-3p2s80s(Box.kt:145)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at com.google.android.horologist.compose.layout.ScrollAwayKt$scrollAwayImpl$1$1.measure-3p2s80s(ScrollAway.kt:132)
  	at androidx.compose.ui.node.BackwardsCompatNode.measure-3p2s80s(BackwardsCompatNode.kt:303)
  	at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:190)
  	at androidx.compose.foundation.layout.FillNode.measure-3p2s80s(Size.kt:721)
  	at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:190)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:699)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.MeasurePassDelegate.measure-BRTryo0(MeasurePassDelegate.kt:470)
  	at androidx.compose.foundation.layout.BoxMeasurePolicy.measure-3p2s80s(Box.kt:168)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
2026-02-19 17:22:17.766  6301-6301  AndroidRuntime          au....shiftyjelly.pocketcasts.debug  E  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172) (Fix with AI)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:699)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.MeasurePassDelegate.measure-BRTryo0(MeasurePassDelegate.kt:470)
  	at androidx.compose.ui.layout.RootMeasurePolicy.measure-3p2s80s(RootMeasurePolicy.kt:37)
  	at androidx.compose.ui.node.InnerNodeCoordinator.measure-BRTryo0(InnerNodeCoordinator.kt:128)
  	at androidx.compose.ui.platform.AndroidComposeView$RootModifierNode.measure-3p2s80s(AndroidComposeView.android.kt:3384)
  	at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:190)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:173)
  	at androidx.compose.ui.node.MeasurePassDelegate$performMeasureBlock$1.invoke(MeasurePassDelegate.kt:172)
  	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:728)
  	at androidx.compose.ui.node.MeasurePassDelegate.remeasure-BRTryo0(MeasurePassDelegate.kt:1064)
  	at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui(LayoutNode.kt:1281)
  	at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:378)
  	at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureOnly(MeasureAndLayoutDelegate.kt:652)
  	at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureOnly(MeasureAndLayoutDelegate.kt:446)
  	at androidx.compose.ui.platform.AndroidComposeView.onMeasure(AndroidComposeView.android.kt:1847)
  	at android.view.View.measure(View.java:28595)
  	at androidx.compose.ui.platform.AbstractComposeView.internalOnMeasure$ui(ComposeView.android.kt:314)
  	at androidx.compose.ui.platform.AbstractComposeView.onMeasure(ComposeView.android.kt:301)
  	at android.view.View.measure(View.java:28595)
  	at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7031)
  	at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
  	at android.view.View.measure(View.java:28595)
  	at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7031)
  	at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1608)
  	at android.widget.LinearLayout.measureVertical(LinearLayout.java:878)
  	at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
  	at android.view.View.measure(View.java:28595)
  	at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7031)
  	at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
  	at com.android.internal.policy.DecorView.onMeasure(DecorView.java:758)
  	at android.view.View.measure(View.java:28595)
  	at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:5088)
  	at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:3436)
  	at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3749)
  	at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:3127)
  	at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:10807)
  	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1630)
  	at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1639)
  	at android.view.Choreographer.doCallbacks(Choreographer.java:1235)
  	at android.view.Choreographer.doFrame(Choreographer.java:1164)
  	at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1613)
  	at android.os.Handler.handleCallback(Handler.java:1070)
  	at android.os.Handler.dispatchMessage(Handler.java:125)
  	at android.os.Looper.dispatchMessage(Looper.java:333)
  	at android.os.Looper.loopOnce(Looper.java:263)
  	at android.os.Looper.loop(Looper.java:367)
  	at android.app.ActivityThread.main(ActivityThread.java:9287)
  	at java.lang.reflect.Method.invoke(Native Method)
  	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:566)
2026-02-19 17:22:17.767  6301-6301  AndroidRuntime          au....shiftyjelly.pocketcasts.debug  E  	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, it never happened on my end. pushed new changes, hopefully that solves the issue!

Copy link
Copy Markdown
Member

@geekygecko geekygecko Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still crashing on my Pixel Watch.

I changed it to the following, and it seems to work.

@Composable
fun TimeTextWithConnectivity(
    isConnected: Boolean,
    modifier: Modifier = Modifier,
) {
    ResponsiveTimeText(
        modifier = modifier,
        startCurvedContent = if (isConnected) null else {
            {
                curvedComposable {
                    OfflineIcon()
                }
            }
        },
        startLinearContent = if (isConnected) null else {
            {
                OfflineIcon()
            }
        },
    )
}

@Composable
fun OfflineIcon() {
    Icon(
        painter = painterResource(IR.drawable.ic_cloud_off),
        contentDescription = "Offline",
        tint = MaterialTheme.colors.onBackground,
        modifier = Modifier.size(16.dp),
    )
}

It looks like the following:

Screenshot_20260221_162803

Comment thread wear/src/main/kotlin/au/com/shiftyjelly/pocketcasts/wear/ui/TimeFormatUtil.kt Outdated
@sztomek sztomek requested a review from geekygecko February 19, 2026 19:48
navController = navController,
state = navState,
AppScaffold(
timeText = { TimeTextWithConnectivity(isConnected = isConnected) },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you scroll down a small amount on the home page and then tap into a sub page, the time isn't positioned correctly. I wonder if we need to set this on the ScreenScaffold components.

Image

@sztomek
Copy link
Copy Markdown
Contributor Author

sztomek commented Feb 26, 2026

closing this one in favor of smaller ones that aim to achieve granular goals

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Wear OS watch app [Type] Enhancement Improve an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants