feat: add omx orchestration backend selection

This commit is contained in:
kris
2026-04-03 03:17:12 +08:00
parent 60f5e2d7d6
commit ec45bed59f
18 changed files with 1993 additions and 20 deletions

View File

@@ -36,8 +36,8 @@ android {
applicationId "com.hyzq.boss"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 21
versionName "2.5.8"
versionCode 22
versionName "2.5.9"
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -126,6 +126,16 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse getProjectOrchestrationBackend(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/orchestration-backend", null);
}
public ApiResponse updateProjectOrchestrationBackend(String projectId, @Nullable String requestedBackendId) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("requestedBackendId", requestedBackendId == null ? JSONObject.NULL : requestedBackendId);
return requestWithRestore("PATCH", "/api/v1/projects/" + encode(projectId) + "/orchestration-backend", payload);
}
public ApiResponse getMasterAgentPromptProfile(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/prompt-profile", null);
}

View File

@@ -48,7 +48,11 @@ public class GroupInfoActivity extends BossScreenActivity {
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json));
BossApiClient.ApiResponse orchestrationResponse = apiClient.getProjectOrchestrationBackend(projectId);
JSONObject orchestrationBackend = orchestrationResponse.ok()
? orchestrationResponse.json
: buildFallbackOrchestrationBackendPayload(orchestrationResponse.message());
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json, orchestrationBackend));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -59,6 +63,10 @@ public class GroupInfoActivity extends BossScreenActivity {
}
private void renderGroup(JSONObject detail, JSONObject participantsPayload) {
renderGroup(detail, participantsPayload, null);
}
private void renderGroup(JSONObject detail, JSONObject participantsPayload, @Nullable JSONObject orchestrationBackendPayload) {
replaceContent();
JSONObject project = detail.optJSONObject("project");
JSONArray participants = participantsPayload.optJSONArray("participants");
@@ -94,6 +102,9 @@ public class GroupInfoActivity extends BossScreenActivity {
null,
v -> openProject(projectId, projectName)
));
if (orchestrationBackendPayload != null) {
appendContent(buildOrchestrationBackendRow(orchestrationBackendPayload));
}
if (repairRequired) {
String meta = invalidParticipantCount > 0
@@ -316,6 +327,171 @@ public class GroupInfoActivity extends BossScreenActivity {
});
}
private LinearLayout buildOrchestrationBackendRow(JSONObject backendPayload) {
String requestedBackendId = backendPayload.optString("requestedBackendId", "boss-native-orchestrator");
String currentBackendId = backendPayload.optString("currentBackendId", requestedBackendId);
JSONObject omxAvailability = backendPayload.optJSONObject("omxAvailability");
String currentLabel = resolveBackendLabel(backendPayload, currentBackendId);
String requestedLabel = resolveBackendLabel(backendPayload, requestedBackendId);
String subtitle = "当前:" + currentLabel;
if (!TextUtils.equals(currentBackendId, requestedBackendId)) {
subtitle += " · 请求:" + requestedLabel;
}
boolean omxSelectable = omxAvailability != null && omxAvailability.optBoolean("selectable", false);
boolean fallbackActive = !TextUtils.equals(currentBackendId, requestedBackendId);
if (omxAvailability != null) {
subtitle += omxSelectable ? " · OMX 可用" : " · OMX 受限";
}
String meta = omxAvailability == null
? "等待后端状态"
: buildOrchestrationBackendAvailabilitySummary(omxAvailability, fallbackActive);
String badge = fallbackActive ? "回退" : (omxSelectable ? "当前" : "受限");
return BossUi.buildWechatMenuRow(
this,
"编排后端",
subtitle,
meta,
badge,
v -> openOrchestrationBackendDialog(backendPayload)
);
}
private void openOrchestrationBackendDialog(JSONObject backendPayload) {
JSONArray availableChoices = backendPayload.optJSONArray("availableChoices");
if (availableChoices == null || availableChoices.length() == 0) {
showMessage("编排后端状态暂不可用");
return;
}
CharSequence[] items = new CharSequence[availableChoices.length()];
final String[] backendIds = new String[availableChoices.length()];
final boolean[] selectable = new boolean[availableChoices.length()];
final String omxReason = backendPayload.optJSONObject("omxAvailability") == null
? "OMX Team Runtime 当前不可用。"
: backendPayload.optJSONObject("omxAvailability").optString("reasonLabel", "OMX Team Runtime 当前不可用。");
final boolean omxSelectable = backendPayload.optJSONObject("omxAvailability") != null
&& backendPayload.optJSONObject("omxAvailability").optBoolean("selectable", false);
for (int i = 0; i < availableChoices.length(); i++) {
JSONObject choice = availableChoices.optJSONObject(i);
if (choice == null) {
items[i] = "未命名后端";
backendIds[i] = "";
selectable[i] = false;
continue;
}
backendIds[i] = choice.optString("backendId", "");
selectable[i] = choice.optBoolean("selectable", false);
String label = resolveBackendLabel(backendPayload, backendIds[i]);
items[i] = label + (selectable[i] ? "" : "(不可用)");
}
new AlertDialog.Builder(this)
.setTitle("选择编排后端")
.setMessage(omxSelectable
? "Boss Native Orchestrator 永远可用OMX Team Runtime 当前可直接切换。"
: "Boss Native Orchestrator 永远可用OMX Team Runtime 当前不可用,切换时会自动回退到 Boss Native Orchestrator。")
.setItems(items, (dialog, which) -> {
String selectedBackendId = backendIds[which];
if (TextUtils.isEmpty(selectedBackendId)) {
showMessage("编排后端选择无效");
return;
}
if (!selectable[which] && TextUtils.equals(selectedBackendId, "omx-team")) {
showMessage(omxReason);
return;
}
saveOrchestrationBackend(selectedBackendId);
})
.setNegativeButton("取消", null)
.show();
}
private void saveOrchestrationBackend(String requestedBackendId) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.updateProjectOrchestrationBackend(projectId, requestedBackendId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("编排后端已更新为 " + resolveBackendLabelForId(requestedBackendId));
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
private String resolveBackendLabel(JSONObject backendPayload, String backendId) {
JSONArray availableChoices = backendPayload.optJSONArray("availableChoices");
if (availableChoices != null) {
for (int i = 0; i < availableChoices.length(); i++) {
JSONObject choice = availableChoices.optJSONObject(i);
if (choice == null) continue;
if (TextUtils.equals(choice.optString("backendId", ""), backendId)) {
return choice.optString("label", resolveBackendLabelForId(backendId));
}
}
}
return resolveBackendLabelForId(backendId);
}
private String resolveBackendLabelForId(String backendId) {
if (TextUtils.equals(backendId, "omx-team")) {
return "OMX Team Runtime";
}
return "Boss Native Orchestrator";
}
private String normalizeOrchestrationReasonLabel(String value) {
String trimmed = value == null ? "" : value.trim();
if (trimmed.endsWith("") || trimmed.endsWith(".")) {
return trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
private String buildOrchestrationBackendAvailabilitySummary(JSONObject omxAvailability, boolean fallbackActive) {
if (omxAvailability.optBoolean("selectable", false)) {
return "OMX Team Runtime 当前可用,当前可切换到该后端。";
}
String reasonLabel = normalizeOrchestrationReasonLabel(
omxAvailability.optString("reasonLabel", "OMX Team Runtime 当前不可用。")
);
return fallbackActive
? reasonLabel + ",当前已自动回退到 Boss Native Orchestrator。"
: reasonLabel + ",切换后会自动回退到 Boss Native Orchestrator。";
}
private JSONObject buildFallbackOrchestrationBackendPayload(String reason) {
try {
JSONArray availableChoices = new JSONArray()
.put(new JSONObject()
.put("backendId", "boss-native-orchestrator")
.put("label", "Boss Native Orchestrator")
.put("selectable", true)
.put("current", true))
.put(new JSONObject()
.put("backendId", "omx-team")
.put("label", "OMX Team Runtime")
.put("selectable", false)
.put("current", false));
return new JSONObject()
.put("currentBackendId", "boss-native-orchestrator")
.put("requestedBackendId", "boss-native-orchestrator")
.put("availableChoices", availableChoices)
.put("omxAvailability", new JSONObject()
.put("selectable", false)
.put("reason", "disabled")
.put("reasonLabel", TextUtils.isEmpty(reason) ? "OMX Team Runtime 当前不可用。" : reason));
} catch (Exception error) {
return new JSONObject();
}
}
private String buildSubtitle(String folderName, int count) {
String memberLabel = count <= 0 ? "暂无成员" : count + " 个成员";
if (folderName.isEmpty()) {

View File

@@ -0,0 +1,244 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import android.content.SharedPreferences;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class BossApiClientOrchestrationBackendTest {
@Test
public void getProjectOrchestrationBackendUsesScopedEndpoint() throws Exception {
RecordingConnection connection = new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/projects/audit-collab/orchestration-backend")
);
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.getProjectOrchestrationBackend("audit-collab");
assertEquals(200, response.statusCode);
assertEquals("/api/v1/projects/audit-collab/orchestration-backend", apiClient.lastPath);
assertEquals("GET", connection.requestMethodValue);
}
@Test
public void updateProjectOrchestrationBackendWritesRequestedBackendId() throws Exception {
RecordingConnection connection = new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/projects/audit-collab/orchestration-backend")
);
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.updateProjectOrchestrationBackend("audit-collab", "omx-team");
assertEquals(200, response.statusCode);
assertEquals("/api/v1/projects/audit-collab/orchestration-backend", apiClient.lastPath);
assertEquals("PATCH", connection.requestMethodValue);
assertEquals("{\"requestedBackendId\":\"omx-team\"}", connection.requestBody());
}
private static final class RecordingBossApiClient extends BossApiClient {
private final RecordingConnection connection;
private String lastPath = "";
RecordingBossApiClient(RecordingConnection connection) {
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
this.connection = connection;
}
@Override
HttpURLConnection openConnection(String path) {
lastPath = path;
return connection;
}
@Override
String encode(String value) {
return value;
}
@Override
void rememberIdentity(JSONObject json) {
// No-op for JVM tests.
}
}
private static final class RecordingConnection extends HttpURLConnection {
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
private final Map<String, String> requestHeaders = new HashMap<>();
private String requestMethodValue = "GET";
RecordingConnection(URL url) {
super(url);
}
@Override
public void disconnect() {}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() {}
@Override
public void setRequestMethod(String method) throws ProtocolException {
requestMethodValue = method;
}
@Override
public void setRequestProperty(String key, String value) {
requestHeaders.put(key, value);
}
@Override
public String getRequestProperty(String key) {
return requestHeaders.get(key);
}
@Override
public OutputStream getOutputStream() {
return requestBody;
}
@Override
public int getResponseCode() {
return 200;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream("{\"ok\":true}".getBytes(StandardCharsets.UTF_8));
}
String requestBody() {
return requestBody.toString(StandardCharsets.UTF_8);
}
}
private static final class InMemorySharedPreferences implements SharedPreferences {
private final Map<String, String> values = new HashMap<>();
@Override
public Map<String, ?> getAll() {
return Collections.unmodifiableMap(values);
}
@Override
public String getString(String key, String defValue) {
return values.getOrDefault(key, defValue);
}
@Override
public Set<String> getStringSet(String key, Set<String> defValues) {
throw new UnsupportedOperationException();
}
@Override
public int getInt(String key, int defValue) {
throw new UnsupportedOperationException();
}
@Override
public long getLong(String key, long defValue) {
throw new UnsupportedOperationException();
}
@Override
public float getFloat(String key, float defValue) {
throw new UnsupportedOperationException();
}
@Override
public boolean getBoolean(String key, boolean defValue) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(String key) {
return values.containsKey(key);
}
@Override
public Editor edit() {
return new Editor() {
@Override
public Editor putString(String key, String value) {
values.put(key, value);
return this;
}
@Override
public Editor remove(String key) {
values.remove(key);
return this;
}
@Override
public Editor clear() {
values.clear();
return this;
}
@Override
public void apply() {}
@Override
public boolean commit() {
return true;
}
@Override
public Editor putStringSet(String key, Set<String> values) {
throw new UnsupportedOperationException();
}
@Override
public Editor putInt(String key, int value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putLong(String key, long value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putFloat(String key, float value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putBoolean(String key, boolean value) {
throw new UnsupportedOperationException();
}
};
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
}
}

View File

@@ -6,6 +6,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
@@ -21,6 +22,22 @@ import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.TimeUnit;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class GroupInfoActivityTest {
@@ -115,6 +132,80 @@ public class GroupInfoActivityTest {
assertTrue(viewTreeContainsText(content, "失效"));
}
@Test
public void renderGroupShowsOrchestrationBackendStateAndFallbackReason() throws Exception {
Intent intent = new Intent()
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
TestGroupInfoActivity activity = Robolectric
.buildActivity(TestGroupInfoActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderGroup",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildOrchestrationBackendPayload())
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "编排后端"));
assertTrue(viewTreeContainsText(content, "当前Boss Native Orchestrator · OMX 可用"));
assertTrue(viewTreeContainsText(content, "OMX Team Runtime 当前可用,当前可切换到该后端。"));
}
@Test
public void renderGroupShowsOmxFallbackHintWhenOmxRuntimeIsUnavailable() throws Exception {
Intent intent = new Intent()
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
TestGroupInfoActivity activity = Robolectric
.buildActivity(TestGroupInfoActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderGroup",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildUnavailableOrchestrationBackendPayload())
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "当前Boss Native Orchestrator · 请求OMX Team Runtime · OMX 受限"));
assertTrue(viewTreeContainsText(content, "OMX Team Runtime 当前不可用,当前已自动回退到 Boss Native Orchestrator。"));
}
@Test
public void saveOrchestrationBackendUsesScopedEndpoint() throws Exception {
Intent intent = new Intent()
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
TestGroupInfoActivity activity = Robolectric
.buildActivity(TestGroupInfoActivity.class, intent)
.setup()
.get();
RecordingConnection connection = new RecordingConnection(
new URL("https://boss.hyzq.net/api/v1/projects/group-1/orchestration-backend")
);
ReflectionHelpers.setField(activity, "apiClient", new RecordingBossApiClient(connection));
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
ReflectionHelpers.callInstanceMethod(
activity,
"saveOrchestrationBackend",
ReflectionHelpers.ClassParameter.from(String.class, "omx-team")
);
Shadows.shadowOf(Looper.getMainLooper()).idle();
assertEquals("/api/v1/projects/group-1/orchestration-backend", connection.lastPath);
assertEquals("PATCH", connection.requestMethodValue);
assertEquals("{\"requestedBackendId\":\"omx-team\"}", connection.requestBody());
}
private static JSONObject buildDetailPayload() throws Exception {
JSONObject threadMeta = new JSONObject()
.put("threadId", "group-thread-3")
@@ -165,6 +256,50 @@ public class GroupInfoActivityTest {
.put("invalidParticipantCount", 1);
}
private static JSONObject buildOrchestrationBackendPayload() throws Exception {
JSONArray availableChoices = new JSONArray()
.put(new JSONObject()
.put("backendId", "boss-native-orchestrator")
.put("label", "Boss Native Orchestrator")
.put("selectable", true)
.put("current", true))
.put(new JSONObject()
.put("backendId", "omx-team")
.put("label", "OMX Team Runtime")
.put("selectable", true)
.put("current", false));
return new JSONObject()
.put("currentBackendId", "boss-native-orchestrator")
.put("requestedBackendId", "boss-native-orchestrator")
.put("availableChoices", availableChoices)
.put("omxAvailability", new JSONObject()
.put("selectable", true)
.put("reason", "ready")
.put("reasonLabel", "OMX Team Runtime 可用。"));
}
private static JSONObject buildUnavailableOrchestrationBackendPayload() throws Exception {
JSONArray availableChoices = new JSONArray()
.put(new JSONObject()
.put("backendId", "boss-native-orchestrator")
.put("label", "Boss Native Orchestrator")
.put("selectable", true)
.put("current", true))
.put(new JSONObject()
.put("backendId", "omx-team")
.put("label", "OMX Team Runtime")
.put("selectable", false)
.put("current", false));
return new JSONObject()
.put("currentBackendId", "boss-native-orchestrator")
.put("requestedBackendId", "omx-team")
.put("availableChoices", availableChoices)
.put("omxAvailability", new JSONObject()
.put("selectable", false)
.put("reason", "script_not_found")
.put("reasonLabel", "OMX Team Runtime 当前不可用。"));
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
@@ -210,4 +345,221 @@ public class GroupInfoActivityTest {
// Tests render the lightweight info state directly.
}
}
private static final class RecordingBossApiClient extends BossApiClient {
private final RecordingConnection connection;
RecordingBossApiClient(RecordingConnection connection) {
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
this.connection = connection;
}
@Override
HttpURLConnection openConnection(String path) {
connection.lastPath = path;
return connection;
}
@Override
String encode(String value) {
return value;
}
}
private static final class RecordingConnection extends HttpURLConnection {
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
private final Map<String, String> requestHeaders = new HashMap<>();
private String requestMethodValue = "GET";
private String lastPath = "";
RecordingConnection(URL url) {
super(url);
}
@Override
public void disconnect() {}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() {}
@Override
public void setRequestMethod(String method) throws ProtocolException {
requestMethodValue = method;
}
@Override
public void setRequestProperty(String key, String value) {
requestHeaders.put(key, value);
}
@Override
public String getRequestProperty(String key) {
return requestHeaders.get(key);
}
@Override
public OutputStream getOutputStream() {
return requestBody;
}
@Override
public int getResponseCode() {
return 200;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream("{\"ok\":true}".getBytes(StandardCharsets.UTF_8));
}
String requestBody() {
return requestBody.toString(StandardCharsets.UTF_8);
}
}
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
private final Map<String, String> values = new HashMap<>();
@Override
public Map<String, ?> getAll() {
return Collections.unmodifiableMap(values);
}
@Override
public String getString(String key, String defValue) {
return values.getOrDefault(key, defValue);
}
@Override
public Set<String> getStringSet(String key, Set<String> defValues) {
throw new UnsupportedOperationException();
}
@Override
public int getInt(String key, int defValue) {
throw new UnsupportedOperationException();
}
@Override
public long getLong(String key, long defValue) {
throw new UnsupportedOperationException();
}
@Override
public float getFloat(String key, float defValue) {
throw new UnsupportedOperationException();
}
@Override
public boolean getBoolean(String key, boolean defValue) {
throw new UnsupportedOperationException();
}
@Override
public boolean contains(String key) {
return values.containsKey(key);
}
@Override
public Editor edit() {
return new Editor() {
@Override
public Editor putString(String key, String value) {
values.put(key, value);
return this;
}
@Override
public Editor remove(String key) {
values.remove(key);
return this;
}
@Override
public Editor clear() {
values.clear();
return this;
}
@Override
public void apply() {}
@Override
public boolean commit() {
return true;
}
@Override
public Editor putStringSet(String key, Set<String> values) {
throw new UnsupportedOperationException();
}
@Override
public Editor putInt(String key, int value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putLong(String key, long value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putFloat(String key, float value) {
throw new UnsupportedOperationException();
}
@Override
public Editor putBoolean(String key, boolean value) {
throw new UnsupportedOperationException();
}
};
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
}
private static final class DirectExecutorService extends AbstractExecutorService {
private boolean shutdown;
@Override
public void shutdown() {
shutdown = true;
}
@Override
public List<Runnable> shutdownNow() {
shutdown = true;
return java.util.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();
}
}
}