Lighten Android chat realtime refreshes
This commit is contained in:
@@ -111,6 +111,10 @@ public class BossApiClient {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId), null);
|
||||
}
|
||||
|
||||
public ApiResponse getProjectMessages(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/messages", null);
|
||||
}
|
||||
|
||||
public ApiResponse getDispatchPlans(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/dispatch-plans", null);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ final class BossRealtimeClient {
|
||||
|
||||
interface Listener {
|
||||
void onRealtimeEvent(BossRealtimeEvent event);
|
||||
|
||||
default void onRealtimeConnectionChanged(boolean connected) {}
|
||||
}
|
||||
|
||||
private final BossApiClient apiClient;
|
||||
@@ -54,7 +56,7 @@ final class BossRealtimeClient {
|
||||
|
||||
synchronized void stop() {
|
||||
running = false;
|
||||
connected = false;
|
||||
setConnected(false);
|
||||
if (activeConnection != null) {
|
||||
activeConnection.disconnect();
|
||||
activeConnection = null;
|
||||
@@ -69,6 +71,14 @@ final class BossRealtimeClient {
|
||||
return connected;
|
||||
}
|
||||
|
||||
private void setConnected(boolean nextConnected) {
|
||||
if (connected == nextConnected) {
|
||||
return;
|
||||
}
|
||||
connected = nextConnected;
|
||||
listener.onRealtimeConnectionChanged(nextConnected);
|
||||
}
|
||||
|
||||
private void runLoop() {
|
||||
long backoffMs = INITIAL_BACKOFF_MS;
|
||||
while (running) {
|
||||
@@ -129,7 +139,7 @@ final class BossRealtimeClient {
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new IOException("REALTIME_STREAM_HTTP_" + statusCode);
|
||||
}
|
||||
connected = true;
|
||||
setConnected(true);
|
||||
Log.i(TAG, "Realtime stream connected");
|
||||
|
||||
try (InputStream inputStream = connection.getInputStream();
|
||||
@@ -152,7 +162,7 @@ final class BossRealtimeClient {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connected = false;
|
||||
setConnected(false);
|
||||
activeConnection = null;
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
@@ -72,12 +72,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private boolean masterAgentReplyTimedOut;
|
||||
private @Nullable String masterAgentReplyBaselineMessageId;
|
||||
private String currentScreenTitle;
|
||||
private String currentBaseSubtitle;
|
||||
private String currentScreenSubtitle;
|
||||
private String projectCollaborationMode = "development";
|
||||
private String projectApprovalState = "not_required";
|
||||
private boolean lightDispatchReminderEnabled;
|
||||
private @Nullable JSONObject currentPendingDispatchPlan;
|
||||
private @Nullable JSONObject currentRejectedDispatchPlan;
|
||||
private @Nullable JSONObject currentParticipantsPayload;
|
||||
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
|
||||
private ActivityResultLauncher<Intent> conversationInfoLauncher;
|
||||
private ActivityResultLauncher<Intent> masterAgentPromptLauncher;
|
||||
@@ -93,7 +95,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private final Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean conversationAutoRefreshArmed;
|
||||
private boolean conversationAutoRefreshEnabled;
|
||||
private boolean lastKnownRealtimeConnected;
|
||||
private boolean realtimeReloadScheduled;
|
||||
private boolean realtimeReloadRequiresFullSnapshot;
|
||||
private boolean reloadInFlight;
|
||||
private boolean pendingReload;
|
||||
private boolean pendingReloadForcedScrollToBottom;
|
||||
@@ -114,7 +118,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
@Override
|
||||
public void run() {
|
||||
realtimeReloadScheduled = false;
|
||||
triggerRealtimeReload();
|
||||
boolean requireFullSnapshot = realtimeReloadRequiresFullSnapshot;
|
||||
realtimeReloadRequiresFullSnapshot = false;
|
||||
triggerRealtimeReload(requireFullSnapshot);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,7 +254,20 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
new ActivityResultContracts.GetContent(),
|
||||
uri -> onAttachmentPicked(uri, "file")
|
||||
);
|
||||
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
|
||||
realtimeClient = new BossRealtimeClient(apiClient, new BossRealtimeClient.Listener() {
|
||||
@Override
|
||||
public void onRealtimeEvent(BossRealtimeEvent event) {
|
||||
handleRealtimeEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRealtimeConnectionChanged(boolean connected) {
|
||||
runOnUiThread(() -> {
|
||||
lastKnownRealtimeConnected = connected;
|
||||
syncRealtimeStatusIndicator();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
BossWindowInsets.applyKeyboardAvoidingInset(composerRow);
|
||||
BossWindowInsets.applyKeyboardAvoidingInset(multiSelectActionsLayout);
|
||||
@@ -299,6 +318,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
conversationAutoRefreshEnabled = true;
|
||||
updateConversationAutoRefresh();
|
||||
updateRealtimeSubscription();
|
||||
syncRealtimeStatusIndicator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -307,6 +327,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
cancelConversationAutoRefresh();
|
||||
cancelRealtimeReloadSchedule();
|
||||
stopRealtimeUpdates();
|
||||
syncRealtimeStatusIndicator();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@@ -335,7 +356,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||
return;
|
||||
}
|
||||
runOnUiThread(this::scheduleRealtimeReload);
|
||||
runOnUiThread(() -> scheduleRealtimeReload(!"project.messages.updated".equals(event.eventName)));
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
@@ -369,11 +390,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
|| "master_agent.task.updated".equals(event.eventName);
|
||||
}
|
||||
|
||||
void triggerRealtimeReload() {
|
||||
reload();
|
||||
void triggerRealtimeReload(boolean requireFullSnapshot) {
|
||||
if (requireFullSnapshot) {
|
||||
reload();
|
||||
return;
|
||||
}
|
||||
reloadMessagesOnly();
|
||||
}
|
||||
|
||||
private void scheduleRealtimeReload() {
|
||||
private void scheduleRealtimeReload(boolean requireFullSnapshot) {
|
||||
if (requireFullSnapshot) {
|
||||
realtimeReloadRequiresFullSnapshot = true;
|
||||
}
|
||||
if (realtimeReloadScheduled) {
|
||||
return;
|
||||
}
|
||||
@@ -387,6 +415,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
private void reload(boolean forcedScrollToBottom) {
|
||||
reloadSnapshot(forcedScrollToBottom, false);
|
||||
}
|
||||
|
||||
private void reloadMessagesOnly() {
|
||||
reloadSnapshot(false, true);
|
||||
}
|
||||
|
||||
private void reloadSnapshot(boolean forcedScrollToBottom, boolean messagesOnly) {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
finish();
|
||||
@@ -403,13 +439,21 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ProjectSnapshot snapshot = loadProjectSnapshotForRefresh();
|
||||
ProjectSnapshot snapshot = messagesOnly
|
||||
? loadProjectMessagesSnapshotForRefresh()
|
||||
: loadProjectSnapshotForRefresh();
|
||||
runOnUiThread(() -> {
|
||||
renderLoadedProjectSnapshot(snapshot);
|
||||
finishReloadCycle();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
if (messagesOnly) {
|
||||
reloadInFlight = false;
|
||||
setRefreshing(false);
|
||||
reload(forcedScrollToBottom);
|
||||
return;
|
||||
}
|
||||
handleProjectReloadFailure(error);
|
||||
finishReloadCycle();
|
||||
});
|
||||
@@ -421,6 +465,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return fetchProjectSnapshot();
|
||||
}
|
||||
|
||||
ProjectSnapshot loadProjectMessagesSnapshotForRefresh() throws Exception {
|
||||
return fetchProjectMessagesSnapshot();
|
||||
}
|
||||
|
||||
void renderLoadedProjectSnapshot(ProjectSnapshot snapshot) {
|
||||
renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload);
|
||||
}
|
||||
@@ -482,10 +530,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
JSONObject agentControls = project == null ? null : project.optJSONObject("agentControls");
|
||||
currentAgentModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("modelOverride", null));
|
||||
currentReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("reasoningEffortOverride", null));
|
||||
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
|
||||
currentRejectedDispatchPlan = currentPendingDispatchPlan == null
|
||||
? ProjectChatUiState.latestRejectedDispatchPlan(dispatchPlans)
|
||||
: null;
|
||||
if (dispatchPlans != null) {
|
||||
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
|
||||
currentRejectedDispatchPlan = currentPendingDispatchPlan == null
|
||||
? ProjectChatUiState.latestRejectedDispatchPlan(dispatchPlans)
|
||||
: null;
|
||||
}
|
||||
if (participantsPayload != null) {
|
||||
currentParticipantsPayload = participantsPayload;
|
||||
}
|
||||
JSONObject effectiveParticipantsPayload = participantsPayload == null
|
||||
? currentParticipantsPayload
|
||||
: participantsPayload;
|
||||
conversationInfoReady = project != null;
|
||||
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
|
||||
@@ -497,8 +553,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
} else if (projectIsGroup && "rejected".equals(projectApprovalState) && currentRejectedDispatchPlan != null) {
|
||||
appendContent(buildRejectedDispatchPlanView(currentRejectedDispatchPlan));
|
||||
}
|
||||
if (projectIsGroup && participantsPayload != null && participantsPayload.optBoolean("repairRequired", false)) {
|
||||
appendContent(buildRepairGroupMembersView(participantsPayload));
|
||||
if (projectIsGroup
|
||||
&& effectiveParticipantsPayload != null
|
||||
&& effectiveParticipantsPayload.optBoolean("repairRequired", false)) {
|
||||
appendContent(buildRepairGroupMembersView(effectiveParticipantsPayload));
|
||||
}
|
||||
|
||||
JSONArray messages = project == null ? null : project.optJSONArray("messages");
|
||||
@@ -538,6 +596,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private void updateRealtimeSubscription() {
|
||||
if (apiClient != null && apiClient.hasSessionHints()) {
|
||||
realtimeClient.start();
|
||||
syncRealtimeStatusIndicator();
|
||||
return;
|
||||
}
|
||||
stopRealtimeUpdates();
|
||||
@@ -547,6 +606,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (realtimeClient != null) {
|
||||
realtimeClient.stop();
|
||||
}
|
||||
syncRealtimeStatusIndicator();
|
||||
}
|
||||
|
||||
private boolean shouldMaintainConversationAutoRefresh() {
|
||||
@@ -1830,11 +1890,42 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
|
||||
private void updateProjectHeader(String title, String subtitle) {
|
||||
currentScreenTitle = title;
|
||||
currentScreenSubtitle = subtitle;
|
||||
currentBaseSubtitle = subtitle;
|
||||
currentScreenSubtitle = withRealtimeStatus(subtitle);
|
||||
if (selectionState != null && selectionState.multiSelecting) {
|
||||
return;
|
||||
}
|
||||
configureScreen(title, subtitle);
|
||||
configureScreen(title, currentScreenSubtitle);
|
||||
}
|
||||
|
||||
private void syncRealtimeStatusIndicator() {
|
||||
boolean connected = isRealtimeConnected();
|
||||
if (lastKnownRealtimeConnected == connected
|
||||
&& currentScreenSubtitle != null
|
||||
&& currentScreenSubtitle.equals(withRealtimeStatus(currentBaseSubtitle))) {
|
||||
return;
|
||||
}
|
||||
lastKnownRealtimeConnected = connected;
|
||||
if (currentScreenTitle == null || currentScreenTitle.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
currentScreenSubtitle = withRealtimeStatus(currentBaseSubtitle);
|
||||
if (selectionState != null && selectionState.multiSelecting) {
|
||||
return;
|
||||
}
|
||||
configureScreen(currentScreenTitle, currentScreenSubtitle);
|
||||
}
|
||||
|
||||
private String withRealtimeStatus(String subtitle) {
|
||||
String baseSubtitle = subtitle == null ? "" : subtitle.trim();
|
||||
if (baseSubtitle.isEmpty()) {
|
||||
return realtimeStatusLabel();
|
||||
}
|
||||
return baseSubtitle + " · " + realtimeStatusLabel();
|
||||
}
|
||||
|
||||
private String realtimeStatusLabel() {
|
||||
return isRealtimeConnected() ? "实时已连接" : "实时重连中";
|
||||
}
|
||||
|
||||
private String joinDeviceNames(@Nullable JSONArray devices) {
|
||||
@@ -2280,6 +2371,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return new ProjectSnapshot(detailResponse.json, dispatchPlans, participantsPayload);
|
||||
}
|
||||
|
||||
private ProjectSnapshot fetchProjectMessagesSnapshot() throws Exception {
|
||||
BossApiClient.ApiResponse response = apiClient.getProjectMessages(projectId);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
return new ProjectSnapshot(response.json, null, null);
|
||||
}
|
||||
|
||||
private void startReplyWait(
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec,
|
||||
boolean includeDispatchPlans,
|
||||
|
||||
Reference in New Issue
Block a user