Refresh device import draft in realtime

This commit is contained in:
kris
2026-04-07 16:16:37 +08:00
parent ef3bf35463
commit 9268f64506
5 changed files with 151 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import java.util.Map;
public class DeviceImportDraftActivity extends BossScreenActivity {
public static final String EXTRA_DEVICE_ID = "device_id";
public static final String EXTRA_DEVICE_NAME = "device_name";
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private String deviceId;
private String deviceName;
@@ -26,6 +27,8 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
private @Nullable JSONObject currentReviewTask;
private final LinkedHashSet<String> selectedCandidateIds = new LinkedHashSet<>();
private final Runnable reviewPollRunnable = this::reload;
private @Nullable BossRealtimeClient realtimeClient;
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -33,9 +36,22 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
deviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
configureScreen("导入项目", deviceName == null ? "选择要导入的 Codex 项目与线程" : deviceName);
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
reload();
}
@Override
protected void onResume() {
super.onResume();
updateRealtimeSubscription();
}
@Override
protected void onPause() {
stopRealtimeUpdates();
super.onPause();
}
@Override
protected void reload() {
if (deviceId == null || deviceId.isEmpty()) {
@@ -64,6 +80,72 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
});
}
private void updateRealtimeSubscription() {
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
realtimeClient.start();
return;
}
stopRealtimeUpdates();
}
private void stopRealtimeUpdates() {
if (realtimeClient != null) {
realtimeClient.stop();
}
}
void handleRealtimeEvent(BossRealtimeEvent event) {
if (event == null || event.eventName.isEmpty()) {
return;
}
if (!shouldReloadForRealtimeEvent(event)) {
return;
}
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
if (eventFingerprint.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
return;
}
runOnUiThread(this::reload);
}
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
if (!"devices.updated".equals(event.eventName)) {
return false;
}
String payloadDeviceId = event.payload.optString("deviceId", "").trim();
if (payloadDeviceId.isEmpty()) {
return true;
}
if (deviceId == null || deviceId.isEmpty()) {
return true;
}
return payloadDeviceId.equals(deviceId);
}
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
pruneRecentRealtimeEvents(now);
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
if (previousEventAt != null && now - previousEventAt < REALTIME_RELOAD_THROTTLE_MS) {
return true;
}
recentRealtimeEventTimestamps.put(eventFingerprint, now);
return false;
}
private void pruneRecentRealtimeEvents(long now) {
java.util.Iterator<Map.Entry<String, Long>> iterator = recentRealtimeEventTimestamps.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Long> entry = iterator.next();
if (now - entry.getValue() >= REALTIME_RELOAD_THROTTLE_MS) {
iterator.remove();
}
}
}
private void applyPayload(
@Nullable JSONObject draft,
@Nullable JSONObject resolution,
@@ -88,6 +170,7 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
@Override
protected void onDestroy() {
contentLayout.removeCallbacks(reviewPollRunnable);
stopRealtimeUpdates();
super.onDestroy();
}

View File

@@ -14,6 +14,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@@ -103,6 +104,62 @@ public class DeviceImportDraftActivityTest {
assertFalse(viewTreeContainsText(content, "树莓派二代接入与联调"));
}
@Test
public void matchingDeviceUpdatedEventTriggersReload() throws Exception {
TestDeviceImportDraftActivity activity = Robolectric
.buildActivity(
TestDeviceImportDraftActivity.class,
new Intent()
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_ID, "device-1")
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_NAME, "Mac Studio")
)
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "device-1"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertTrue(activity.reloadCount == 1);
}
@Test
public void unrelatedDeviceEventDoesNotTriggerReload() throws Exception {
TestDeviceImportDraftActivity activity = Robolectric
.buildActivity(
TestDeviceImportDraftActivity.class,
new Intent()
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_ID, "device-1")
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_NAME, "Mac Studio")
)
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "device-2"))
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertTrue(activity.reloadCount == 0);
}
private static JSONObject buildPendingDraft() throws Exception {
return new JSONObject()
.put("draftId", "draft-1")
@@ -222,9 +279,16 @@ public class DeviceImportDraftActivityTest {
}
public static class TestDeviceImportDraftActivity extends DeviceImportDraftActivity {
private boolean reloadEnabled;
private int reloadCount;
@Override
protected void reload() {
// Tests render synthetic payloads directly.
if (!reloadEnabled) {
return;
}
reloadCount += 1;
setRefreshing(false);
}
}
}