diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index 79b77ef..3d8de3b 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -104,6 +104,8 @@ public class MainActivity extends AppCompatActivity { private boolean conversationQuickActionsVisible = false; private boolean conversationAutoRefreshArmed = false; private boolean conversationAutoRefreshEnabled = false; + private boolean rootTabRefreshInFlight = false; + private boolean pendingRootTabRefresh = false; private final java.util.HashMap recentRealtimeEventTimestamps = new java.util.HashMap<>(); private final Set selectedConversationProjectIds = new LinkedHashSet<>(); private @Nullable RootPagerAdapter rootPagerAdapter; @@ -362,6 +364,11 @@ public class MainActivity extends AppCompatActivity { } void refreshCurrentTab() { + if (rootTabRefreshInFlight) { + pendingRootTabRefresh = true; + return; + } + rootTabRefreshInFlight = true; if ("devices".equals(activeTab)) { refreshDevicesData(); return; @@ -413,9 +420,13 @@ public class MainActivity extends AppCompatActivity { if (!finalConversationsOk) { showMessage("刷新失败,请稍后重试"); } + completeRealtimeTabRefresh(); }); } catch (Exception error) { - runOnUiThread(() -> handleSessionRefreshFailure()); + runOnUiThread(() -> { + handleSessionRefreshFailure(); + completeRealtimeTabRefresh(); + }); } }); } @@ -448,9 +459,13 @@ public class MainActivity extends AppCompatActivity { if (!finalDevicesOk) { showMessage("刷新失败,请稍后重试"); } + completeRealtimeTabRefresh(); }); } catch (Exception error) { - runOnUiThread(() -> handleSessionRefreshFailure()); + runOnUiThread(() -> { + handleSessionRefreshFailure(); + completeRealtimeTabRefresh(); + }); } }); } @@ -501,13 +516,26 @@ public class MainActivity extends AppCompatActivity { if (!finalOtaOk && !finalSettingsOk) { showMessage("刷新失败,请稍后重试"); } + completeRealtimeTabRefresh(); }); } catch (Exception error) { - runOnUiThread(() -> handleSessionRefreshFailure()); + runOnUiThread(() -> { + handleSessionRefreshFailure(); + completeRealtimeTabRefresh(); + }); } }); } + void completeRealtimeTabRefresh() { + rootTabRefreshInFlight = false; + if (!pendingRootTabRefresh) { + return; + } + pendingRootTabRefresh = false; + refreshCurrentTab(); + } + void handleRealtimeEvent(BossRealtimeEvent event) { if (event == null || event.eventName.isEmpty()) { return; diff --git a/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java b/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java index 8730651..fcc5509 100644 --- a/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java @@ -2,6 +2,7 @@ package com.hyzq.boss; import static org.junit.Assert.assertEquals; +import android.os.Looper; import org.json.JSONObject; import org.junit.Test; import org.junit.runner.RunWith; @@ -11,6 +12,8 @@ import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; +import java.util.function.BooleanSupplier; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 34) public class MainActivityRealtimeTest { @@ -171,6 +174,62 @@ public class MainActivityRealtimeTest { assertEquals(1, activity.meRefreshCount); } + @Test + public void burstConversationRealtimeEventsCoalesceIntoSingleFollowUpRefresh() throws Exception { + TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get(); + ReflectionHelpers.callInstanceMethod(activity, "showContent"); + ReflectionHelpers.setField(activity, "rootTabRefreshInFlight", true); + + ReflectionHelpers.callInstanceMethod( + activity, + "handleRealtimeEvent", + ReflectionHelpers.ClassParameter.from( + BossRealtimeEvent.class, + new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1")) + ) + ); + Shadows.shadowOf(activity.getMainLooper()).idle(); + + ReflectionHelpers.callInstanceMethod( + activity, + "handleRealtimeEvent", + ReflectionHelpers.ClassParameter.from( + BossRealtimeEvent.class, + new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1")) + ) + ); + ReflectionHelpers.callInstanceMethod( + activity, + "handleRealtimeEvent", + ReflectionHelpers.ClassParameter.from( + BossRealtimeEvent.class, + new BossRealtimeEvent("conversation.context_indicator.updated", new JSONObject().put("projectId", "project-1")) + ) + ); + Shadows.shadowOf(activity.getMainLooper()).idle(); + + assertEquals(0, activity.conversationRefreshCount); + + activity.completeRealtimeTabRefresh(); + waitFor(() -> activity.conversationRefreshCount == 1); + + assertEquals(1, activity.conversationRefreshCount); + assertEquals(0, activity.deviceRefreshCount); + assertEquals(0, activity.meRefreshCount); + } + + private static void waitFor(BooleanSupplier condition) throws Exception { + long deadlineAt = System.currentTimeMillis() + 2_000L; + while (System.currentTimeMillis() < deadlineAt) { + Shadows.shadowOf(Looper.getMainLooper()).idle(); + if (condition.getAsBoolean()) { + return; + } + Thread.sleep(20L); + } + throw new AssertionError("condition not met before timeout"); + } + public static class TestMainActivity extends MainActivity { int conversationRefreshCount; int deviceRefreshCount; @@ -179,16 +238,19 @@ public class MainActivityRealtimeTest { @Override void refreshConversationsData() { conversationRefreshCount += 1; + completeRealtimeTabRefresh(); } @Override void refreshDevicesData() { deviceRefreshCount += 1; + completeRealtimeTabRefresh(); } @Override void refreshMeData() { meRefreshCount += 1; + completeRealtimeTabRefresh(); } } }