Refresh device import draft in realtime
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"fileName": "boss-android-v2.5.11-release.apk",
|
||||
"urlPath": "/api/v1/user/ota/package",
|
||||
"sizeBytes": 3354908,
|
||||
"updatedAt": "2026-04-07T08:07:13Z",
|
||||
"sha256": "519c26f094aa0babb102ff557e8ed44f2f97290133765300f2f3ba1766923290",
|
||||
"sizeBytes": 3355587,
|
||||
"updatedAt": "2026-04-07T08:13:31Z",
|
||||
"sha256": "0a94ce924344c02914f78414ad6bb5276126124e8eae123508a52e2c33aa86d7",
|
||||
"versionName": "2.5.11",
|
||||
"versionCode": 24,
|
||||
"buildFlavor": "release"
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user