Xiaomi: Suppress premature activity-data-finish signal during two-phase fetch

The Xiaomi two-phase activity fetch (today + past) could fire the data-finish
signal that triggers downstream consumers before all DETAILS files had been
parsed, so a consumer reading the database in that gap saw an incomplete
picture. Hold the finish signal until both fetch phases have delivered and
all files are parsed.

Use an AtomicBoolean for the fetch-hold flag and set awaitingPastResponse
before the fetch starts to avoid signalling between the two phases.
This commit is contained in:
Gideon Zenz
2026-06-05 14:55:20 +02:00
parent 75e25a0e90
commit 00c0a2ba5c
2 changed files with 43 additions and 6 deletions
@@ -34,6 +34,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.PriorityQueue; import java.util.PriorityQueue;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -52,6 +53,8 @@ public class XiaomiActivityFileFetcher {
private final Queue<XiaomiActivityFileId> mFetchQueue = new PriorityQueue<>(); private final Queue<XiaomiActivityFileId> mFetchQueue = new PriorityQueue<>();
private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream(); private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
private boolean isFetching = false; private boolean isFetching = false;
private volatile boolean awaitingPastResponse = false;
private final AtomicBoolean queueHeld = new AtomicBoolean(false);
private final Handler timeoutHandler = new Handler(Looper.getMainLooper()); private final Handler timeoutHandler = new Handler(Looper.getMainLooper());
@@ -61,6 +64,8 @@ public class XiaomiActivityFileFetcher {
public void dispose() { public void dispose() {
clearTimeout(); clearTimeout();
awaitingPastResponse = false;
queueHeld.set(false);
} }
private void clearTimeout() { private void clearTimeout() {
@@ -71,6 +76,7 @@ public class XiaomiActivityFileFetcher {
// #4305 - Set the timeout in case the watch does not send the file // #4305 - Set the timeout in case the watch does not send the file
this.timeoutHandler.postDelayed(() -> { this.timeoutHandler.postDelayed(() -> {
LOG.warn("Timed out waiting for activity file with {} bytes in the buffer", mBuffer.size()); LOG.warn("Timed out waiting for activity file with {} bytes in the buffer", mBuffer.size());
awaitingPastResponse = false;
triggerNextFetch(); triggerNextFetch();
}, 5000L); }, 5000L);
} }
@@ -157,6 +163,30 @@ public class XiaomiActivityFileFetcher {
triggerNextFetch(); triggerNextFetch();
} }
public void setAwaitingPastResponse(final boolean awaiting) {
this.awaitingPastResponse = awaiting;
}
public boolean isFetching() {
return isFetching;
}
public void signalComplete() {
LOG.debug("Nothing more to fetch");
isFetching = false;
queueHeld.set(false);
mHealthService.getSupport().getDevice().unsetBusyTask();
GB.signalActivityDataFinish(mHealthService.getSupport().getDevice());
GB.updateTransferNotification(null, "", false, 100, mHealthService.getSupport().getContext());
mHealthService.getSupport().getDevice().sendDeviceUpdateIntent(mHealthService.getSupport().getContext());
}
public void resumeFetching() {
if (queueHeld.compareAndSet(true, false)) {
triggerNextFetch();
}
}
public void fetch(final List<XiaomiActivityFileId> fileIds) { public void fetch(final List<XiaomiActivityFileId> fileIds) {
// #4305 - ensure unique files // #4305 - ensure unique files
for (final XiaomiActivityFileId fileId : fileIds) { for (final XiaomiActivityFileId fileId : fileIds) {
@@ -186,12 +216,12 @@ public class XiaomiActivityFileFetcher {
final XiaomiActivityFileId fileId = mFetchQueue.poll(); final XiaomiActivityFileId fileId = mFetchQueue.poll();
if (fileId == null) { if (fileId == null) {
LOG.debug("Nothing more to fetch"); if (awaitingPastResponse) {
isFetching = false; LOG.debug("Queue empty but awaiting past fetch response, holding signal");
mHealthService.getSupport().getDevice().unsetBusyTask(); queueHeld.set(true);
GB.signalActivityDataFinish(mHealthService.getSupport().getDevice()); return;
GB.updateTransferNotification(null, "", false, 100, mHealthService.getSupport().getContext()); }
mHealthService.getSupport().getDevice().sendDeviceUpdateIntent(mHealthService.getSupport().getContext()); signalComplete();
return; return;
} }
@@ -869,11 +869,18 @@ public class XiaomiHealthService extends AbstractXiaomiService {
} }
fileIds.add(fileId); fileIds.add(fileId);
} }
if (subtype == CMD_ACTIVITY_FETCH_TODAY) {
activityFetcher.setAwaitingPastResponse(true);
}
activityFetcher.fetch(fileIds); activityFetcher.fetch(fileIds);
if (subtype == CMD_ACTIVITY_FETCH_TODAY) { if (subtype == CMD_ACTIVITY_FETCH_TODAY) {
LOG.debug("Fetch recorded data from the past"); LOG.debug("Fetch recorded data from the past");
fetchRecordedDataPast(); fetchRecordedDataPast();
} else if (subtype == CMD_ACTIVITY_FETCH_PAST) {
activityFetcher.setAwaitingPastResponse(false);
activityFetcher.resumeFetching();
} }
} }