Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,15 @@ private const val TAG = "SyncSettingsActivity"
// of flipping to "due now".
private const val NEXT_SYNC_REFRESH_INTERVAL_MS = 30 * 1000L

internal fun formatSyncStatus(status: SyncStatus?, dateFormat: DateFormat): String {
if (status == null) return "Last sync: never"
internal fun formatSyncStatus(
status: SyncStatus?,
dateFormat: DateFormat,
isPushOnlyDevice: Boolean = false,
): String {
// Android is push-only until sync v2 ships; append a fixed note so users know
// pull is expected to be empty and won't arrive until v2.
val pushOnlyNote = if (isPushOnlyDevice) "\nPush-only on this device (pull arrives with sync v2)" else ""
if (status == null) return "Last sync: never$pushOnlyNote"

val whenText = dateFormat.format(Date(status.completedAt))
val headline = if (status.success) {
Expand All @@ -47,8 +54,8 @@ internal fun formatSyncStatus(status: SyncStatus?, dateFormat: DateFormat): Stri
}
// Runs recorded before the SyncReport crossed the JNI boundary carry no
// counts; showing "pulled 0, pushed 0" for them would invent an answer.
if (!status.hasReport) return headline
return "$headline\n${formatSyncDetail(status)}"
if (!status.hasReport) return "$headline$pushOnlyNote"
return "$headline\n${formatSyncDetail(status, isPushOnlyDevice)}$pushOnlyNote"
}

/**
Expand Down Expand Up @@ -80,7 +87,7 @@ internal fun formatNextSyncStatus(
* this, a pass that transferred nothing is indistinguishable from one that
* transferred everything — the failure mode of a boolean-only status.
*/
internal fun formatSyncDetail(status: SyncStatus): String {
internal fun formatSyncDetail(status: SyncStatus, isPushOnlyDevice: Boolean = false): String {
val peers = status.peersImported + status.peersSkipped + status.peersFailed
val parts = mutableListOf("pulled ${status.eventsPulled}, pushed ${status.eventsPushed}")
if (peers > 0) {
Expand All @@ -90,8 +97,12 @@ internal fun formatSyncDetail(status: SyncStatus): String {
parts += peerText
}
val line = parts.joinToString(" · ")
if (status.warnings.isEmpty()) return line
return (listOf(line) + status.warnings).joinToString("\n")
// The "zero peers in a configured sync dir" warning is a desktop diagnostic
// (aw-server-rust#687) — not meaningful on Android where pull is a no-op by
// construction (SAF mirror is outbound-only until sync v2).
val warnings = if (isPushOnlyDevice) status.warnings.filter { "zero peers" !in it } else status.warnings
if (warnings.isEmpty()) return line
return (listOf(line) + warnings).joinToString("\n")
}

class SyncSettingsActivity : AppCompatActivity() {
Expand Down Expand Up @@ -251,6 +262,7 @@ class SyncSettingsActivity : AppCompatActivity() {
tvLastSyncStatus.text = formatSyncStatus(
prefs.getLastSyncStatus(),
combinedDateTimeFormat(),
isPushOnlyDevice = true,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,61 @@ class SyncSettingsActivityTest {
assertEquals("first warning", status.warnings[0])
}

@Test
fun formatSyncStatus_pushOnlyDevice_suppressesZeroPeersWarning() {
// aw-server-rust#687 "zero peers" warning is desktop-only; Android is push-only
// until sync v2, so the warning must be filtered and replaced by a push-only note.
assertEquals(
"Last sync succeeded at 2026-09-01 01:30\n" +
"pulled 0, pushed 0\n" +
"Push-only on this device (pull arrives with sync v2)",
formatSyncStatus(
SyncStatus(
completedAt = 1_788_226_200_000L,
success = true,
hasReport = true,
warnings = listOf(
"zero peers in a configured sync dir is usually a layout or setup problem",
),
),
dateFormat,
isPushOnlyDevice = true,
),
)
}

@Test
fun formatSyncStatus_pushOnlyDevice_preservesOtherWarnings() {
// Non-zero-peers warnings (e.g. "push aborted after pull failure") still surface.
assertEquals(
"Last sync succeeded at 2026-09-01 01:30\n" +
"pulled 0, pushed 0\n" +
"push aborted after pull failure\n" +
"Push-only on this device (pull arrives with sync v2)",
formatSyncStatus(
SyncStatus(
completedAt = 1_788_226_200_000L,
success = true,
hasReport = true,
warnings = listOf(
"zero peers in a configured sync dir is usually a layout or setup problem",
"push aborted after pull failure",
),
),
dateFormat,
isPushOnlyDevice = true,
),
)
}

@Test
fun formatSyncStatus_pushOnlyDevice_appendsNoteEvenWhenNeverSynced() {
assertEquals(
"Last sync: never\nPush-only on this device (pull arrives with sync v2)",
formatSyncStatus(null, dateFormat, isPushOnlyDevice = true),
)
}

@Test
fun fromJniResponse_ignoresNegativeCounts() {
val status = SyncStatus.fromJniResponse(
Expand Down
Loading