fix: recover thread takeover session on android
This commit is contained in:
@@ -49,13 +49,12 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(projectId);
|
||||
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
LoadedConversation loadedConversation = loadConversation();
|
||||
BossApiClient.ApiResponse detailResponse = loadedConversation.detailResponse;
|
||||
BossApiClient.ApiResponse participantsResponse = loadedConversation.participantsResponse;
|
||||
JSONObject threadStatusPayload = null;
|
||||
try {
|
||||
BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId);
|
||||
BossApiClient.ApiResponse threadStatusResponse = loadedConversation.threadStatusResponse;
|
||||
if (threadStatusResponse.ok()) {
|
||||
threadStatusPayload = threadStatusResponse.json;
|
||||
}
|
||||
@@ -351,7 +350,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings(
|
||||
BossApiClient.ApiResponse response = saveTakeoverSettingsWithRetry(
|
||||
projectId,
|
||||
enabled,
|
||||
null
|
||||
@@ -373,6 +372,56 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private LoadedConversation loadConversation() throws Exception {
|
||||
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(projectId);
|
||||
if (isUnauthorized(detailResponse)) {
|
||||
BossApiClient.ApiResponse loginResponse = apiClient.autoLogin();
|
||||
if (!loginResponse.ok()) {
|
||||
throw new IllegalStateException(loginResponse.message());
|
||||
}
|
||||
detailResponse = apiClient.getProjectDetail(projectId);
|
||||
}
|
||||
if (!detailResponse.ok()) {
|
||||
throw new IllegalStateException(detailResponse.message());
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (!participantsResponse.ok()) {
|
||||
throw new IllegalStateException(participantsResponse.message());
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId);
|
||||
return new LoadedConversation(detailResponse, participantsResponse, threadStatusResponse);
|
||||
}
|
||||
|
||||
private BossApiClient.ApiResponse saveTakeoverSettingsWithRetry(
|
||||
String targetProjectId,
|
||||
boolean takeoverEnabled,
|
||||
@Nullable Boolean globalTakeoverEnabled
|
||||
) throws Exception {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings(
|
||||
targetProjectId,
|
||||
takeoverEnabled,
|
||||
globalTakeoverEnabled
|
||||
);
|
||||
if (!isUnauthorized(response)) {
|
||||
return response;
|
||||
}
|
||||
BossApiClient.ApiResponse loginResponse = apiClient.autoLogin();
|
||||
if (!loginResponse.ok()) {
|
||||
return response;
|
||||
}
|
||||
return apiClient.updateProjectTakeoverSettings(
|
||||
targetProjectId,
|
||||
takeoverEnabled,
|
||||
globalTakeoverEnabled
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isUnauthorized(@Nullable BossApiClient.ApiResponse response) {
|
||||
return response != null && response.statusCode == 401 && "UNAUTHORIZED".equals(response.message());
|
||||
}
|
||||
|
||||
private String buildSubtitle(@Nullable JSONObject threadMeta, int count) {
|
||||
String folder = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
String suffix = count <= 0 ? "暂无参与线程" : count + " 个参与线程";
|
||||
@@ -410,4 +459,20 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
}
|
||||
return project.optString("id", "");
|
||||
}
|
||||
|
||||
private static final class LoadedConversation {
|
||||
private final BossApiClient.ApiResponse detailResponse;
|
||||
private final BossApiClient.ApiResponse participantsResponse;
|
||||
private final BossApiClient.ApiResponse threadStatusResponse;
|
||||
|
||||
private LoadedConversation(
|
||||
BossApiClient.ApiResponse detailResponse,
|
||||
BossApiClient.ApiResponse participantsResponse,
|
||||
BossApiClient.ApiResponse threadStatusResponse
|
||||
) {
|
||||
this.detailResponse = detailResponse;
|
||||
this.participantsResponse = participantsResponse;
|
||||
this.threadStatusResponse = threadStatusResponse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -25,6 +26,12 @@ import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowDialog;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
@@ -164,6 +171,68 @@ public class ConversationInfoActivityTest {
|
||||
assertTrue(viewTreeContainsText(listView.getAdapter().getView(1, null, listView), "刷新"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reloadAutoRecoversUnauthorizedAndRendersConversationInfo() {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestConversationInfoActivity activity = Robolectric
|
||||
.buildActivity(TestConversationInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(
|
||||
activity.getSharedPreferences("conversation-info-auth-test", Context.MODE_PRIVATE),
|
||||
"https://boss.hyzq.net"
|
||||
);
|
||||
ReflectionHelpers.setField(activity, "apiClient", apiClient);
|
||||
ReflectionHelpers.setField(activity, "reloadEnabled", true);
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
activity.reload();
|
||||
ShadowLooper.shadowMainLooper().idle();
|
||||
|
||||
assertEquals(2, apiClient.detailCalls);
|
||||
assertEquals(1, apiClient.autoLoginCalls);
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertFalse(viewTreeContainsTextFragment(content, "会话信息加载失败"));
|
||||
assertTrue(content.getChildCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTakeoverAutoRecoversUnauthorizedAndPersistsSetting() {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestConversationInfoActivity activity = Robolectric
|
||||
.buildActivity(TestConversationInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(
|
||||
activity.getSharedPreferences("conversation-info-save-auth-test", Context.MODE_PRIVATE),
|
||||
"https://boss.hyzq.net"
|
||||
);
|
||||
apiClient.failFirstLoad = false;
|
||||
apiClient.failFirstSave = true;
|
||||
ReflectionHelpers.setField(activity, "apiClient", apiClient);
|
||||
ReflectionHelpers.setField(activity, "reloadEnabled", true);
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
activity.reload();
|
||||
ShadowLooper.shadowMainLooper().idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"saveTakeoverSetting",
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false)
|
||||
);
|
||||
ShadowLooper.shadowMainLooper().idle();
|
||||
|
||||
assertEquals(1, apiClient.updateTakeoverCalls);
|
||||
assertEquals(1, apiClient.retryUpdateTakeoverCalls);
|
||||
assertEquals(1, apiClient.autoLoginCalls);
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload() throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "thread-7")
|
||||
@@ -271,9 +340,143 @@ public class ConversationInfoActivityTest {
|
||||
}
|
||||
|
||||
public static class TestConversationInfoActivity extends ConversationInfoActivity {
|
||||
private boolean reloadEnabled;
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render the lightweight info state directly.
|
||||
if (!reloadEnabled) {
|
||||
return;
|
||||
}
|
||||
super.reload();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingBossApiClient extends BossApiClient {
|
||||
private int detailCalls;
|
||||
private int autoLoginCalls;
|
||||
private int updateTakeoverCalls;
|
||||
private int retryUpdateTakeoverCalls;
|
||||
private boolean failFirstLoad = true;
|
||||
private boolean failFirstSave;
|
||||
|
||||
RecordingBossApiClient(android.content.SharedPreferences prefs, String baseUrl) {
|
||||
super(prefs, baseUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse getProjectDetail(String projectId) throws java.io.IOException, org.json.JSONException {
|
||||
detailCalls += 1;
|
||||
if (failFirstLoad && detailCalls == 1) {
|
||||
return ApiResponse.error(
|
||||
401,
|
||||
new JSONObject()
|
||||
.put("ok", false)
|
||||
.put("message", "UNAUTHORIZED")
|
||||
);
|
||||
}
|
||||
try {
|
||||
JSONObject payload = buildDetailPayload();
|
||||
payload.put("ok", true);
|
||||
return new ApiResponse(200, payload);
|
||||
} catch (Exception error) {
|
||||
throw new java.io.IOException("BUILD_DETAIL_PAYLOAD_FAILED", error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse getConversationParticipants(String projectId) throws java.io.IOException, org.json.JSONException {
|
||||
try {
|
||||
JSONObject payload = buildParticipantsPayload();
|
||||
payload.put("ok", true);
|
||||
return new ApiResponse(200, payload);
|
||||
} catch (Exception error) {
|
||||
throw new java.io.IOException("BUILD_PARTICIPANTS_PAYLOAD_FAILED", error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse getThreadStatus(String projectId) throws java.io.IOException, org.json.JSONException {
|
||||
try {
|
||||
JSONObject payload = buildThreadStatusPayload();
|
||||
payload.put("ok", true);
|
||||
return new ApiResponse(200, payload);
|
||||
} catch (Exception error) {
|
||||
throw new java.io.IOException("BUILD_THREAD_STATUS_PAYLOAD_FAILED", error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse autoLogin() throws java.io.IOException, org.json.JSONException {
|
||||
autoLoginCalls += 1;
|
||||
return new ApiResponse(
|
||||
200,
|
||||
new JSONObject()
|
||||
.put("ok", true)
|
||||
.put("session", new JSONObject().put("account", "17600003315"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse updateProjectTakeoverSettings(
|
||||
String projectId,
|
||||
Boolean takeoverEnabled,
|
||||
Boolean globalTakeoverEnabled
|
||||
) throws java.io.IOException, org.json.JSONException {
|
||||
if (failFirstSave && updateTakeoverCalls == 0) {
|
||||
updateTakeoverCalls += 1;
|
||||
return ApiResponse.error(
|
||||
401,
|
||||
new JSONObject()
|
||||
.put("ok", false)
|
||||
.put("message", "UNAUTHORIZED")
|
||||
);
|
||||
}
|
||||
if (failFirstSave) {
|
||||
retryUpdateTakeoverCalls += 1;
|
||||
} else {
|
||||
updateTakeoverCalls += 1;
|
||||
}
|
||||
return new ApiResponse(
|
||||
200,
|
||||
new JSONObject()
|
||||
.put("ok", true)
|
||||
.put("controls", new JSONObject().put("takeoverEnabled", takeoverEnabled))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||||
private boolean shutdown;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown = true;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user