Refresh OTA screen in realtime
This commit is contained in:
@@ -19,8 +19,12 @@ import androidx.annotation.Nullable;
|
|||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class AboutActivity extends BossScreenActivity {
|
public class AboutActivity extends BossScreenActivity {
|
||||||
private static final long OTA_PROGRESS_POLL_INTERVAL_MS = 1_000L;
|
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 OTA_UI_PREFS = "boss_native_client";
|
||||||
private static final String KEY_ACTIVE_DOWNLOAD_ID = "ota_active_download_id";
|
private static final String KEY_ACTIVE_DOWNLOAD_ID = "ota_active_download_id";
|
||||||
private static final String KEY_COMPLETED_DOWNLOAD_ID = "ota_completed_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 int lastDownloadStatus = -1;
|
||||||
private long lastDownloadedBytes = 0L;
|
private long lastDownloadedBytes = 0L;
|
||||||
private long lastTotalBytes = -1L;
|
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 Handler otaProgressHandler = new Handler(Looper.getMainLooper());
|
||||||
private final Runnable otaProgressPoller = new Runnable() {
|
private final Runnable otaProgressPoller = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -68,6 +74,7 @@ public class AboutActivity extends BossScreenActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
configureScreen("关于", "版本与 OTA 更新");
|
configureScreen("关于", "版本与 OTA 更新");
|
||||||
restoreDownloadUiState();
|
restoreDownloadUiState();
|
||||||
|
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
|
||||||
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
registerReceiver(otaDownloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
registerReceiver(otaDownloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||||
@@ -77,8 +84,21 @@ public class AboutActivity extends BossScreenActivity {
|
|||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
updateRealtimeSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
stopRealtimeUpdates();
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
stopRealtimeUpdates();
|
||||||
otaProgressHandler.removeCallbacks(otaProgressPoller);
|
otaProgressHandler.removeCallbacks(otaProgressPoller);
|
||||||
try {
|
try {
|
||||||
unregisterReceiver(otaDownloadReceiver);
|
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) {
|
private void renderAbout(@Nullable JSONObject user, JSONObject ota) {
|
||||||
replaceContent();
|
replaceContent();
|
||||||
otaPayload = ota;
|
otaPayload = ota;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"fileName": "boss-android-v2.5.11-release.apk",
|
"fileName": "boss-android-v2.5.11-release.apk",
|
||||||
"urlPath": "/api/v1/user/ota/package",
|
"urlPath": "/api/v1/user/ota/package",
|
||||||
"sizeBytes": 3356442,
|
"sizeBytes": 3357009,
|
||||||
"updatedAt": "2026-04-07T08:39:26Z",
|
"updatedAt": "2026-04-07T08:49:27Z",
|
||||||
"sha256": "292abbd4f6e7953b3e8d8f195ee1c463d16e973a2d81e7a9760a71951167d47a",
|
"sha256": "7fe774cacef70758f227273dc4774e065e9eebba48e90a88d9c814b8f315a00d",
|
||||||
"versionName": "2.5.11",
|
"versionName": "2.5.11",
|
||||||
"versionCode": 24,
|
"versionCode": 24,
|
||||||
"buildFlavor": "release"
|
"buildFlavor": "release"
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user