Refresh OTA screen in realtime

This commit is contained in:
kris
2026-04-07 16:53:22 +08:00
parent b5d6495017
commit 4f59d59014
5 changed files with 165 additions and 3 deletions

View File

@@ -19,8 +19,12 @@ import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.LinkedHashMap;
import java.util.Map;
public class AboutActivity extends BossScreenActivity {
private static final long OTA_PROGRESS_POLL_INTERVAL_MS = 1_000L;
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private static final String OTA_UI_PREFS = "boss_native_client";
private static final String KEY_ACTIVE_DOWNLOAD_ID = "ota_active_download_id";
private static final String KEY_COMPLETED_DOWNLOAD_ID = "ota_completed_download_id";
@@ -38,6 +42,8 @@ public class AboutActivity extends BossScreenActivity {
private int lastDownloadStatus = -1;
private long lastDownloadedBytes = 0L;
private long lastTotalBytes = -1L;
private @Nullable BossRealtimeClient realtimeClient;
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
private final Handler otaProgressHandler = new Handler(Looper.getMainLooper());
private final Runnable otaProgressPoller = new Runnable() {
@Override
@@ -68,6 +74,7 @@ public class AboutActivity extends BossScreenActivity {
super.onCreate(savedInstanceState);
configureScreen("关于", "版本与 OTA 更新");
restoreDownloadUiState();
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(otaDownloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
@@ -77,8 +84,21 @@ public class AboutActivity extends BossScreenActivity {
reload();
}
@Override
protected void onResume() {
super.onResume();
updateRealtimeSubscription();
}
@Override
protected void onPause() {
stopRealtimeUpdates();
super.onPause();
}
@Override
protected void onDestroy() {
stopRealtimeUpdates();
otaProgressHandler.removeCallbacks(otaProgressPoller);
try {
unregisterReceiver(otaDownloadReceiver);
@@ -111,6 +131,58 @@ public class AboutActivity 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 (!"ota.updated".equals(event.eventName)) {
return;
}
String eventFingerprint = BossRealtimeClient.buildEventFingerprint(event);
if (eventFingerprint.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
return;
}
runOnUiThread(this::reload);
}
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 renderAbout(@Nullable JSONObject user, JSONObject ota) {
replaceContent();
otaPayload = ota;

View File

@@ -0,0 +1,90 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Intent;
import org.json.JSONObject;
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 java.lang.reflect.Method;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class AboutActivityTest {
@Test
public void otaUpdatedEventTriggersReload() throws Exception {
TestAboutActivity activity = Robolectric
.buildActivity(TestAboutActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
Method handleRealtimeEvent = findHandleRealtimeEvent();
assertNotNull(handleRealtimeEvent);
handleRealtimeEvent.invoke(
activity,
new BossRealtimeEvent("ota.updated", new JSONObject().put("deviceId", "mac-studio"))
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(1, activity.reloadCount);
}
@Test
public void unrelatedConversationEventDoesNotTriggerReload() throws Exception {
TestAboutActivity activity = Robolectric
.buildActivity(TestAboutActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
Method handleRealtimeEvent = findHandleRealtimeEvent();
assertNotNull(handleRealtimeEvent);
handleRealtimeEvent.invoke(
activity,
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "master-agent"))
);
Shadows.shadowOf(activity.getMainLooper()).idle();
assertEquals(0, activity.reloadCount);
}
private static Method findHandleRealtimeEvent() {
for (Method method : AboutActivity.class.getDeclaredMethods()) {
if ("handleRealtimeEvent".equals(method.getName())
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0] == BossRealtimeEvent.class) {
method.setAccessible(true);
return method;
}
}
return null;
}
public static class TestAboutActivity extends AboutActivity {
private boolean reloadEnabled;
private int reloadCount;
@Override
protected void reload() {
if (!reloadEnabled) {
return;
}
reloadCount += 1;
setRefreshing(false);
}
}
}

View File

@@ -1,9 +1,9 @@
{
"fileName": "boss-android-v2.5.11-release.apk",
"urlPath": "/api/v1/user/ota/package",
"sizeBytes": 3356442,
"updatedAt": "2026-04-07T08:39:26Z",
"sha256": "292abbd4f6e7953b3e8d8f195ee1c463d16e973a2d81e7a9760a71951167d47a",
"sizeBytes": 3357009,
"updatedAt": "2026-04-07T08:49:27Z",
"sha256": "7fe774cacef70758f227273dc4774e065e9eebba48e90a88d9c814b8f315a00d",
"versionName": "2.5.11",
"versionCode": 24,
"buildFlavor": "release"