feat: restore wechat thread ui and group chat
This commit is contained in:
@@ -33,8 +33,8 @@ android {
|
||||
applicationId "com.hyzq.boss"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10
|
||||
versionName "2.2.1"
|
||||
versionCode 11
|
||||
versionName "2.3.0"
|
||||
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
<activity android:name=".ProjectVersionsActivity" android:exported="false" />
|
||||
<activity android:name=".ProjectForwardActivity" android:exported="false" />
|
||||
<activity android:name=".ThreadDetailActivity" android:exported="false" />
|
||||
<activity android:name=".ConversationInfoActivity" android:exported="false" />
|
||||
<activity android:name=".GroupInfoActivity" android:exported="false" />
|
||||
<activity android:name=".GroupCreateActivity" android:exported="false" />
|
||||
<activity android:name=".DeviceDetailActivity" android:exported="false" />
|
||||
<activity android:name=".DeviceEnrollmentActivity" android:exported="false" />
|
||||
<activity android:name=".SkillInventoryActivity" android:exported="false" />
|
||||
|
||||
@@ -25,6 +25,7 @@ public class AboutActivity extends BossScreenActivity {
|
||||
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_LAST_DOWNLOAD_FILE_NAME = "ota_last_download_file_name";
|
||||
private static final String KEY_LAST_DOWNLOAD_VERSION = "ota_last_download_version";
|
||||
private static final String KEY_LAST_DOWNLOAD_STATUS = "ota_last_download_status";
|
||||
|
||||
private long activeDownloadId = -1L;
|
||||
@@ -33,6 +34,7 @@ public class AboutActivity extends BossScreenActivity {
|
||||
private @Nullable LinearLayout otaDownloadStateSection;
|
||||
private @Nullable Uri downloadedApkUri;
|
||||
private @Nullable String lastDownloadFileName;
|
||||
private @Nullable String lastDownloadVersion;
|
||||
private int lastDownloadStatus = -1;
|
||||
private long lastDownloadedBytes = 0L;
|
||||
private long lastTotalBytes = -1L;
|
||||
@@ -64,7 +66,7 @@ public class AboutActivity extends BossScreenActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
configureScreen("关于", "版本与更新");
|
||||
configureScreen("关于", "版本与 OTA 更新");
|
||||
restoreDownloadUiState();
|
||||
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -93,14 +95,12 @@ public class AboutActivity extends BossScreenActivity {
|
||||
try {
|
||||
BossApiClient.ApiResponse settings = apiClient.getSettings();
|
||||
BossApiClient.ApiResponse ota = apiClient.getOtaStatus();
|
||||
BossApiClient.ApiResponse session = apiClient.getSession();
|
||||
if (!settings.ok() || !ota.ok() || !session.ok()) {
|
||||
if (!settings.ok() || !ota.ok()) {
|
||||
throw new IllegalStateException("PROFILE_OR_OTA_LOAD_FAILED");
|
||||
}
|
||||
runOnUiThread(() -> renderAbout(
|
||||
settings.json.optJSONObject("user"),
|
||||
ota.json,
|
||||
session.json.optJSONObject("session")
|
||||
ota.json
|
||||
));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -111,80 +111,164 @@ public class AboutActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void renderAbout(@Nullable JSONObject user, JSONObject ota, @Nullable JSONObject session) {
|
||||
private void renderAbout(@Nullable JSONObject user, JSONObject ota) {
|
||||
replaceContent();
|
||||
otaPayload = ota;
|
||||
if (user != null) {
|
||||
appendContent(BossUi.buildListRow(
|
||||
this,
|
||||
"当前版本",
|
||||
user.optString("version", "-")
|
||||
+ " · 账号 " + user.optString("account", "-"),
|
||||
"绑定 Codex " + user.optString("boundCodexNodeLabel", "未绑定")
|
||||
+ (session == null ? "" : " · 会话到期 " + session.optString("expiresAt", "-")),
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
JSONObject availableRelease = ota.optJSONObject("availableRelease");
|
||||
String otaSubtitle = availableRelease == null
|
||||
? "当前已经是最新版本。"
|
||||
: "可用版本 " + availableRelease.optString("version", "未知版本");
|
||||
String otaMeta = availableRelease == null
|
||||
? "当前版本 " + ota.optString("currentVersion", "-")
|
||||
: availableRelease.optString("summary", "暂无摘要")
|
||||
+ " · 文件 " + availableRelease.optString("packageFileName", "-");
|
||||
appendContent(BossUi.buildListRow(
|
||||
invalidateStaleDownloadedApk(ota.optJSONObject("availableRelease"));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"OTA 状态",
|
||||
otaSubtitle,
|
||||
otaMeta,
|
||||
availableRelease == null ? null : "NEW",
|
||||
"当前版本",
|
||||
user == null ? ota.optString("currentVersion", "-") : user.optString("version", ota.optString("currentVersion", "-")),
|
||||
"已安装版本",
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildMenuRow(this, "检查更新", "拉取最新 OTA 状态", null, v -> performOtaAction("check")));
|
||||
appendContent(BossUi.buildMenuRow(this, "登记应用 OTA", "把当前已应用版本写回服务端", null, v -> performOtaAction("apply")));
|
||||
appendContent(BossUi.buildMenuRow(this, "应用内下载 APK", "下载最新安装包并拉起系统安装器", null, v -> downloadLatestApk()));
|
||||
JSONObject availableRelease = ota.optJSONObject("availableRelease");
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"OTA 状态",
|
||||
buildOtaStatusSubtitle(ota),
|
||||
buildOtaStatusMeta(ota),
|
||||
availableRelease == null ? null : "OTA",
|
||||
null
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"OTA 更新内容",
|
||||
buildOtaContentBody(ota),
|
||||
availableRelease == null ? "没有可下载的新版本时,可直接点按钮检查更新。" : "下载完成后会自动拉起系统安装器。"
|
||||
));
|
||||
|
||||
android.widget.Button otaButton = BossUi.buildPrimaryButton(this, resolvePrimaryOtaActionLabel(availableRelease));
|
||||
otaButton.setEnabled(activeDownloadId <= 0);
|
||||
otaButton.setOnClickListener(v -> performPrimaryOtaAction(availableRelease));
|
||||
appendContent(otaButton);
|
||||
|
||||
appendContent(BossUi.buildMenuRow(this, "重新检查更新", "拉取最新 OTA 状态", null, v -> performOtaAction("check")));
|
||||
if (downloadedApkUri != null || completedDownloadId > 0) {
|
||||
appendContent(BossUi.buildMenuRow(
|
||||
this,
|
||||
"同步已应用状态",
|
||||
"安装完成后点这里,把服务端 OTA 状态更新为已应用",
|
||||
null,
|
||||
v -> performOtaAction("apply")
|
||||
));
|
||||
}
|
||||
otaDownloadStateSection = new LinearLayout(this);
|
||||
otaDownloadStateSection.setOrientation(LinearLayout.VERTICAL);
|
||||
appendContent(otaDownloadStateSection);
|
||||
refreshDownloadStateSection();
|
||||
appendContent(BossUi.buildMenuRow(
|
||||
this,
|
||||
WechatSurfaceMapper.advancedEntryTitle(),
|
||||
"进入运维对话、审计与修复入口",
|
||||
null,
|
||||
v -> startActivity(new Intent(this, OpsCenterActivity.class))
|
||||
));
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
JSONArray logs = ota.optJSONArray("logs");
|
||||
if (logs != null) {
|
||||
for (int i = 0; i < logs.length(); i++) {
|
||||
JSONObject log = logs.optJSONObject(i);
|
||||
if (log == null) continue;
|
||||
appendContent(BossUi.buildListRow(
|
||||
this,
|
||||
log.optString("version", "OTA"),
|
||||
log.optString("summary", ""),
|
||||
log.optString("status", "-") + " · " + log.optString("createdAt", "-"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
private static String buildOtaStatusSubtitle(JSONObject ota) {
|
||||
JSONObject availableRelease = ota.optJSONObject("availableRelease");
|
||||
if (availableRelease == null) {
|
||||
return "当前已经是最新版本。";
|
||||
}
|
||||
return "发现新版本 " + availableRelease.optString("version", "未知版本");
|
||||
}
|
||||
|
||||
private static String buildOtaStatusMeta(JSONObject ota) {
|
||||
JSONObject availableRelease = ota.optJSONObject("availableRelease");
|
||||
if (availableRelease == null) {
|
||||
return "当前版本 " + ota.optString("currentVersion", "-");
|
||||
}
|
||||
String summaryLine = firstSummaryLine(availableRelease.optJSONArray("summary"));
|
||||
return availableRelease.optString("packageFileName", "boss-android-latest.apk")
|
||||
+ (summaryLine.isEmpty() ? "" : " · " + summaryLine);
|
||||
}
|
||||
|
||||
private static String buildOtaContentBody(JSONObject ota) {
|
||||
JSONObject availableRelease = ota.optJSONObject("availableRelease");
|
||||
if (availableRelease != null) {
|
||||
JSONArray lines = availableRelease.optJSONArray("summary");
|
||||
if (lines != null && lines.length() > 0) {
|
||||
StringBuilder builder = new StringBuilder("版本 ").append(availableRelease.optString("version", "-"));
|
||||
for (int i = 0; i < lines.length(); i++) {
|
||||
String line = lines.optString(i);
|
||||
if (line == null || line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
builder.append("\n").append(i + 1).append(". ").append(line);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
String note = availableRelease.optString("note", "");
|
||||
if (!note.isEmpty()) {
|
||||
return "版本 " + availableRelease.optString("version", "-") + "\n" + note;
|
||||
}
|
||||
}
|
||||
setRefreshing(false);
|
||||
|
||||
JSONArray logs = ota.optJSONArray("logs");
|
||||
if (logs != null && logs.length() > 0) {
|
||||
JSONObject latest = logs.optJSONObject(0);
|
||||
if (latest != null) {
|
||||
String note = latest.optString("note", "");
|
||||
if (!note.isEmpty()) {
|
||||
return latest.optString("version", "当前版本") + "\n" + note;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "当前没有待更新内容,点击下方按钮可重新检查更新。";
|
||||
}
|
||||
|
||||
private static String firstSummaryLine(@Nullable JSONArray lines) {
|
||||
if (lines == null || lines.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
for (int i = 0; i < lines.length(); i++) {
|
||||
String line = lines.optString(i);
|
||||
if (line != null && !line.isEmpty()) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String resolvePrimaryOtaActionLabel(@Nullable JSONObject availableRelease) {
|
||||
if (activeDownloadId > 0) {
|
||||
return "下载中…";
|
||||
}
|
||||
if (downloadedApkUri != null) {
|
||||
return "安装更新";
|
||||
}
|
||||
if (availableRelease != null) {
|
||||
return "立即 OTA";
|
||||
}
|
||||
return "检查更新";
|
||||
}
|
||||
|
||||
private void performPrimaryOtaAction(@Nullable JSONObject availableRelease) {
|
||||
if (activeDownloadId > 0) {
|
||||
return;
|
||||
}
|
||||
if (downloadedApkUri != null) {
|
||||
installDownloadedApk();
|
||||
return;
|
||||
}
|
||||
if (availableRelease != null) {
|
||||
downloadLatestApk();
|
||||
return;
|
||||
}
|
||||
performOtaAction("check");
|
||||
}
|
||||
|
||||
private void performOtaAction(String action) {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = "check".equals(action) ? apiClient.checkOta() : apiClient.applyOta();
|
||||
BossApiClient.ApiResponse response = "apply".equals(action)
|
||||
? apiClient.applyOta()
|
||||
: apiClient.checkOta();
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
showMessage("check".equals(action) ? "已完成版本检查" : "已登记 OTA 应用");
|
||||
if ("apply".equals(action)) {
|
||||
clearLocalOtaDownloadState();
|
||||
}
|
||||
showMessage("apply".equals(action) ? "已同步 OTA 应用状态" : "已完成版本检查");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
@@ -196,6 +280,19 @@ public class AboutActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void clearLocalOtaDownloadState() {
|
||||
activeDownloadId = -1L;
|
||||
completedDownloadId = -1L;
|
||||
downloadedApkUri = null;
|
||||
lastDownloadFileName = null;
|
||||
lastDownloadVersion = null;
|
||||
lastDownloadStatus = -1;
|
||||
lastDownloadedBytes = 0L;
|
||||
lastTotalBytes = -1L;
|
||||
otaProgressHandler.removeCallbacks(otaProgressPoller);
|
||||
persistDownloadUiState();
|
||||
}
|
||||
|
||||
private void downloadLatestApk() {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -224,6 +321,9 @@ public class AboutActivity extends BossScreenActivity {
|
||||
String fileName = availableRelease == null
|
||||
? "boss-android-latest.apk"
|
||||
: availableRelease.optString("packageFileName", "boss-android-latest.apk");
|
||||
String releaseVersion = availableRelease == null
|
||||
? null
|
||||
: availableRelease.optString("version", null);
|
||||
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apiClient.getProtectedOtaPackageUrl()));
|
||||
request.setTitle(fileName);
|
||||
@@ -237,6 +337,7 @@ public class AboutActivity extends BossScreenActivity {
|
||||
|
||||
downloadedApkUri = null;
|
||||
lastDownloadFileName = fileName;
|
||||
lastDownloadVersion = releaseVersion;
|
||||
lastDownloadStatus = DownloadManager.STATUS_PENDING;
|
||||
lastDownloadedBytes = 0L;
|
||||
lastTotalBytes = -1L;
|
||||
@@ -345,6 +446,8 @@ public class AboutActivity extends BossScreenActivity {
|
||||
|
||||
@Nullable
|
||||
private OtaDownloadStateMapper.UiState resolveDownloadUiState() {
|
||||
JSONObject availableRelease = otaPayload == null ? null : otaPayload.optJSONObject("availableRelease");
|
||||
invalidateStaleDownloadedApk(availableRelease);
|
||||
String fileName = resolveDownloadFileName();
|
||||
if (activeDownloadId > 0) {
|
||||
DownloadProgressSnapshot snapshot = queryDownloadProgress(activeDownloadId);
|
||||
@@ -435,6 +538,7 @@ public class AboutActivity extends BossScreenActivity {
|
||||
activeDownloadId = prefs.getLong(KEY_ACTIVE_DOWNLOAD_ID, -1L);
|
||||
completedDownloadId = prefs.getLong(KEY_COMPLETED_DOWNLOAD_ID, -1L);
|
||||
lastDownloadFileName = prefs.getString(KEY_LAST_DOWNLOAD_FILE_NAME, null);
|
||||
lastDownloadVersion = prefs.getString(KEY_LAST_DOWNLOAD_VERSION, null);
|
||||
lastDownloadStatus = prefs.getInt(KEY_LAST_DOWNLOAD_STATUS, -1);
|
||||
if (completedDownloadId > 0) {
|
||||
DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
|
||||
@@ -454,10 +558,91 @@ public class AboutActivity extends BossScreenActivity {
|
||||
.putLong(KEY_ACTIVE_DOWNLOAD_ID, activeDownloadId)
|
||||
.putLong(KEY_COMPLETED_DOWNLOAD_ID, completedDownloadId)
|
||||
.putString(KEY_LAST_DOWNLOAD_FILE_NAME, lastDownloadFileName)
|
||||
.putString(KEY_LAST_DOWNLOAD_VERSION, lastDownloadVersion)
|
||||
.putInt(KEY_LAST_DOWNLOAD_STATUS, lastDownloadStatus)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void invalidateStaleDownloadedApk(@Nullable JSONObject availableRelease) {
|
||||
long[] downloadIds = collectStaleDownloadIdsForRemoval(
|
||||
availableRelease,
|
||||
lastDownloadFileName,
|
||||
lastDownloadVersion,
|
||||
downloadedApkUri != null || completedDownloadId > 0 || activeDownloadId > 0,
|
||||
activeDownloadId,
|
||||
completedDownloadId
|
||||
);
|
||||
if (downloadIds.length == 0) {
|
||||
return;
|
||||
}
|
||||
removeStaleDownloadTasks(downloadIds);
|
||||
clearLocalOtaDownloadState();
|
||||
}
|
||||
|
||||
private void removeStaleDownloadTasks(long[] downloadIds) {
|
||||
if (downloadIds.length == 0) {
|
||||
return;
|
||||
}
|
||||
DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
manager.remove(downloadIds);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Keep UI state recoverable even if DownloadManager cleanup fails.
|
||||
}
|
||||
}
|
||||
|
||||
static long[] collectStaleDownloadIdsForRemoval(
|
||||
@Nullable JSONObject availableRelease,
|
||||
@Nullable String downloadedFileName,
|
||||
@Nullable String downloadedVersion,
|
||||
boolean hasLocalDownload,
|
||||
long activeId,
|
||||
long completedId
|
||||
) {
|
||||
if (!hasLocalDownload) {
|
||||
return new long[0];
|
||||
}
|
||||
if (isDownloadedReleaseCurrent(availableRelease, downloadedFileName, downloadedVersion)) {
|
||||
return new long[0];
|
||||
}
|
||||
return collectDownloadIdsForRemoval(activeId, completedId);
|
||||
}
|
||||
|
||||
private static long[] collectDownloadIdsForRemoval(long activeId, long completedId) {
|
||||
if (activeId > 0 && completedId > 0) {
|
||||
if (activeId == completedId) {
|
||||
return new long[]{activeId};
|
||||
}
|
||||
return new long[]{activeId, completedId};
|
||||
}
|
||||
if (activeId > 0) {
|
||||
return new long[]{activeId};
|
||||
}
|
||||
if (completedId > 0) {
|
||||
return new long[]{completedId};
|
||||
}
|
||||
return new long[0];
|
||||
}
|
||||
|
||||
private static boolean isDownloadedReleaseCurrent(
|
||||
@Nullable JSONObject availableRelease,
|
||||
@Nullable String downloadedFileName,
|
||||
@Nullable String downloadedVersion
|
||||
) {
|
||||
if (availableRelease == null) {
|
||||
return false;
|
||||
}
|
||||
String releaseFileName = availableRelease.optString("packageFileName", "");
|
||||
String releaseVersion = availableRelease.optString("version", "");
|
||||
if (releaseFileName.isEmpty() || releaseVersion.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return releaseFileName.equals(downloadedFileName) && releaseVersion.equals(downloadedVersion);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DownloadProgressSnapshot queryDownloadProgress(long downloadId) {
|
||||
DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
|
||||
|
||||
@@ -49,13 +49,11 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
JSONArray accounts = payload.optJSONArray("accounts");
|
||||
JSONObject activeIdentity = payload.optJSONObject("activeIdentity");
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildListRow(
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"账号管理",
|
||||
"管理主 GPT、备用 GPT 与 API 容灾。",
|
||||
"支持新增、编辑、激活、校验和删除。",
|
||||
null,
|
||||
null
|
||||
"AI 账号",
|
||||
"这里统一管理主 GPT、备用 GPT 与 API 容灾账号。",
|
||||
"轻点条目可编辑,按钮可切换、校验或删除。"
|
||||
));
|
||||
appendContent(buildActiveIdentityCard(activeIdentity));
|
||||
appendContent(buildAccountsSection(accounts));
|
||||
@@ -64,25 +62,29 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
|
||||
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
|
||||
if (activeIdentity == null) {
|
||||
return BossUi.buildListRow(this, "当前主控身份", "当前没有可用账号。", "请先新增或启用一个账号。", null, null);
|
||||
return BossUi.buildSoftPanel(this, "当前主控身份", "当前没有可用账号。", "请先新增或启用一个账号。");
|
||||
}
|
||||
String subtitle = activeIdentity.optString("label", "AI 账号")
|
||||
+ " · " + activeIdentity.optString("displayName", "-");
|
||||
String meta = activeIdentity.optString("roleLabel", "-")
|
||||
+ " · " + activeIdentity.optString("providerLabel", "-")
|
||||
+ " · " + activeIdentity.optString("statusLabel", "-");
|
||||
return BossUi.buildListRow(this, "当前主控身份", subtitle, meta, "当前", null);
|
||||
String body = activeIdentity.optString("label", "AI 账号")
|
||||
+ " · " + activeIdentity.optString("displayName", "-")
|
||||
+ "\n" + activeIdentity.optString("roleLabel", "-")
|
||||
+ " · " + activeIdentity.optString("providerLabel", "-");
|
||||
return BossUi.buildSoftPanel(
|
||||
this,
|
||||
"当前主控身份",
|
||||
body,
|
||||
activeIdentity.optString("statusLabel", "-")
|
||||
);
|
||||
}
|
||||
|
||||
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
|
||||
LinearLayout section = new LinearLayout(this);
|
||||
section.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
section.addView(BossUi.buildListRow(
|
||||
section.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"账号列表",
|
||||
accounts == null || accounts.length() == 0 ? "当前还没有 AI 账号。" : "点开可编辑,按钮可激活、校验或删除。",
|
||||
"当前 API:/api/v1/accounts",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
@@ -116,7 +118,7 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
|
||||
LinearLayout card = new LinearLayout(this);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.addView(BossUi.buildListRow(
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
account.optString("label", "未命名账号"),
|
||||
subtitle.toString(),
|
||||
@@ -125,22 +127,16 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
v -> openAccountEditor(account, null)
|
||||
));
|
||||
|
||||
Button activate = BossUi.buildPrimaryButton(this, account.optBoolean("isActive") ? "已激活" : "设为当前主控");
|
||||
Button activate = BossUi.buildMiniActionButton(this, account.optBoolean("isActive") ? "当前主控" : "设为当前", !account.optBoolean("isActive"));
|
||||
activate.setEnabled(!account.optBoolean("isActive"));
|
||||
activate.setOnClickListener(v -> activateAccount(account));
|
||||
card.addView(activate);
|
||||
|
||||
Button validate = BossUi.buildSecondaryButton(this, "校验连接");
|
||||
Button validate = BossUi.buildMiniActionButton(this, "校验连接", false);
|
||||
validate.setOnClickListener(v -> validateAccount(account));
|
||||
card.addView(validate);
|
||||
|
||||
Button edit = BossUi.buildSecondaryButton(this, "编辑账号");
|
||||
edit.setOnClickListener(v -> openAccountEditor(account, null));
|
||||
card.addView(edit);
|
||||
|
||||
Button delete = BossUi.buildSecondaryButton(this, "删除账号");
|
||||
Button delete = BossUi.buildMiniActionButton(this, "删除账号", false);
|
||||
delete.setOnClickListener(v -> confirmDeleteAccount(account));
|
||||
card.addView(delete);
|
||||
card.addView(BossUi.buildInlineActionRow(this, activate, validate, delete));
|
||||
|
||||
return card;
|
||||
}
|
||||
@@ -182,18 +178,18 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
|
||||
LinearLayout form = new LinearLayout(this);
|
||||
form.setOrientation(LinearLayout.VERTICAL);
|
||||
form.addView(labelInput);
|
||||
form.addView(displayNameInput);
|
||||
form.addView(accountIdentifierInput);
|
||||
form.addView(nodeIdInput);
|
||||
form.addView(nodeLabelInput);
|
||||
form.addView(modelInput);
|
||||
form.addView(apiKeyInput);
|
||||
form.addView(loginStatusInput);
|
||||
form.addView(roleSpinner);
|
||||
form.addView(providerSpinner);
|
||||
form.addView(enabledSwitch);
|
||||
form.addView(setActiveSwitch);
|
||||
form.addView(BossUi.buildFormCell(this, "标签", "例如 主 GPT", labelInput));
|
||||
form.addView(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表中", displayNameInput));
|
||||
form.addView(BossUi.buildFormCell(this, "账号标识", "邮箱、登录名或备注信息", accountIdentifierInput));
|
||||
form.addView(BossUi.buildFormCell(this, "节点 ID", "Master Codex Node 的唯一标识", nodeIdInput));
|
||||
form.addView(BossUi.buildFormCell(this, "节点名称", "用于快速识别节点", nodeLabelInput));
|
||||
form.addView(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput));
|
||||
form.addView(BossUi.buildFormCell(this, "API Key", "仅 OpenAI API 模式需要", apiKeyInput));
|
||||
form.addView(BossUi.buildFormCell(this, "登录状态备注", "可记录 Plus、有无风控等状态", loginStatusInput));
|
||||
form.addView(BossUi.buildFormCell(this, "账号角色", null, roleSpinner));
|
||||
form.addView(BossUi.buildFormCell(this, "提供方", null, providerSpinner));
|
||||
form.addView(BossUi.buildFormCell(this, "启用状态", null, enabledSwitch));
|
||||
form.addView(BossUi.buildFormCell(this, "保存后动作", null, setActiveSwitch));
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(existing == null ? "新增 AI 账号" : "编辑 AI 账号")
|
||||
|
||||
@@ -72,6 +72,21 @@ public class BossApiClient {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId), null);
|
||||
}
|
||||
|
||||
public ApiResponse renameConversation(String projectId, String name, boolean group) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("name", name);
|
||||
payload.put("mode", group ? "group" : "thread");
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/rename", payload);
|
||||
}
|
||||
|
||||
public ApiResponse createGroupChat(String projectId, JSONObject payload) throws IOException, JSONException {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/group-chat", payload == null ? new JSONObject() : payload);
|
||||
}
|
||||
|
||||
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
|
||||
}
|
||||
|
||||
public ApiResponse sendProjectMessage(String projectId, String body, String kind) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("body", body);
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -16,6 +22,22 @@ import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class BossUi {
|
||||
private static final int[] AVATAR_BG_COLORS = {
|
||||
Color.parseColor("#1EC76F"),
|
||||
Color.parseColor("#DFF5EC"),
|
||||
Color.parseColor("#DCEEF6"),
|
||||
Color.parseColor("#FCE5BF")
|
||||
};
|
||||
private static final int[] ACTIVITY_ICON_COLORS = {
|
||||
Color.parseColor("#C4CCC8"),
|
||||
Color.parseColor("#B5C0BA"),
|
||||
Color.parseColor("#A7B3AD"),
|
||||
Color.parseColor("#98A6A0")
|
||||
};
|
||||
private static final int DEVICE_STATUS_ONLINE = Color.parseColor("#18B85A");
|
||||
private static final int DEVICE_STATUS_ABNORMAL = Color.parseColor("#FF5A5A");
|
||||
private static final int DEVICE_STATUS_OFFLINE = Color.parseColor("#A7AFB7");
|
||||
|
||||
private BossUi() {}
|
||||
|
||||
public static LinearLayout buildListRow(
|
||||
@@ -167,10 +189,482 @@ public final class BossUi {
|
||||
return buildListRow(context, title, description, null, badge, listener);
|
||||
}
|
||||
|
||||
public static LinearLayout buildWechatMenuRow(
|
||||
Context context,
|
||||
String title,
|
||||
@Nullable String subtitle,
|
||||
@Nullable String meta,
|
||||
@Nullable String badge,
|
||||
@Nullable View.OnClickListener listener
|
||||
) {
|
||||
return buildListRow(context, title, subtitle, meta, badge, listener);
|
||||
}
|
||||
|
||||
public static LinearLayout buildSimpleProfileHeader(
|
||||
Context context,
|
||||
String name,
|
||||
String subtitle,
|
||||
@Nullable String detail
|
||||
) {
|
||||
LinearLayout card = new LinearLayout(context);
|
||||
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 12);
|
||||
card.setLayoutParams(params);
|
||||
card.setPadding(dp(context, 16), dp(context, 16), dp(context, 16), dp(context, 16));
|
||||
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
|
||||
card.setElevation(dp(context, 1));
|
||||
|
||||
TextView avatar = new TextView(context);
|
||||
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, 64), dp(context, 64));
|
||||
avatar.setLayoutParams(avatarParams);
|
||||
avatar.setGravity(Gravity.CENTER);
|
||||
avatar.setText(firstLetter(name));
|
||||
avatar.setTextSize(28);
|
||||
avatar.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
avatar.setTextColor(context.getColor(R.color.boss_green));
|
||||
avatar.setBackground(createRoundedBackground(Color.parseColor("#DFF3E8"), dp(context, 18)));
|
||||
card.addView(avatar);
|
||||
|
||||
LinearLayout textWrap = new LinearLayout(context);
|
||||
textWrap.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
textParams.leftMargin = dp(context, 14);
|
||||
textWrap.setLayoutParams(textParams);
|
||||
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(TextUtils.isEmpty(name) ? "我的" : name);
|
||||
titleView.setTextSize(17);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
textWrap.addView(titleView);
|
||||
|
||||
TextView subtitleView = new TextView(context);
|
||||
subtitleView.setText(subtitle);
|
||||
subtitleView.setTextSize(13);
|
||||
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
subtitleView.setPadding(0, dp(context, 4), 0, 0);
|
||||
textWrap.addView(subtitleView);
|
||||
|
||||
if (!TextUtils.isEmpty(detail)) {
|
||||
TextView detailView = new TextView(context);
|
||||
detailView.setText(detail);
|
||||
detailView.setTextSize(12);
|
||||
detailView.setTextColor(context.getColor(R.color.boss_text_soft));
|
||||
detailView.setPadding(0, dp(context, 6), 0, 0);
|
||||
textWrap.addView(detailView);
|
||||
}
|
||||
|
||||
card.addView(textWrap);
|
||||
return card;
|
||||
}
|
||||
|
||||
public static LinearLayout buildFormCell(
|
||||
Context context,
|
||||
String label,
|
||||
@Nullable String helper,
|
||||
View field
|
||||
) {
|
||||
LinearLayout cell = new LinearLayout(context);
|
||||
cell.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 10);
|
||||
cell.setLayoutParams(params);
|
||||
cell.setPadding(dp(context, 16), dp(context, 14), dp(context, 16), dp(context, 14));
|
||||
cell.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
|
||||
|
||||
TextView labelView = new TextView(context);
|
||||
labelView.setText(label);
|
||||
labelView.setTextSize(14);
|
||||
labelView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
labelView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
cell.addView(labelView);
|
||||
|
||||
if (!TextUtils.isEmpty(helper)) {
|
||||
TextView helperView = new TextView(context);
|
||||
helperView.setText(helper);
|
||||
helperView.setTextSize(12);
|
||||
helperView.setTextColor(context.getColor(R.color.boss_text_soft));
|
||||
helperView.setPadding(0, dp(context, 4), 0, dp(context, 10));
|
||||
cell.addView(helperView);
|
||||
}
|
||||
|
||||
field.setPadding(field.getPaddingLeft(), field.getPaddingTop(), field.getPaddingRight(), field.getPaddingBottom());
|
||||
cell.addView(field);
|
||||
return cell;
|
||||
}
|
||||
|
||||
public static LinearLayout buildDeviceCard(
|
||||
Context context,
|
||||
WechatSurfaceMapper.DeviceRow row,
|
||||
@Nullable View.OnClickListener listener,
|
||||
@Nullable View.OnLongClickListener longClickListener
|
||||
) {
|
||||
LinearLayout card = new LinearLayout(context);
|
||||
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 12);
|
||||
card.setLayoutParams(params);
|
||||
card.setPadding(dp(context, 14), dp(context, 14), dp(context, 14), dp(context, 14));
|
||||
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
|
||||
card.setElevation(dp(context, 1));
|
||||
if (listener != null) {
|
||||
card.setClickable(true);
|
||||
card.setFocusable(true);
|
||||
card.setOnClickListener(listener);
|
||||
}
|
||||
if (longClickListener != null) {
|
||||
card.setOnLongClickListener(longClickListener);
|
||||
}
|
||||
|
||||
FrameLayout avatarWrap = new FrameLayout(context);
|
||||
LinearLayout.LayoutParams avatarWrapParams = new LinearLayout.LayoutParams(dp(context, 56), dp(context, 56));
|
||||
avatarWrap.setLayoutParams(avatarWrapParams);
|
||||
|
||||
TextView avatar = new TextView(context);
|
||||
FrameLayout.LayoutParams avatarParams = new FrameLayout.LayoutParams(dp(context, 52), dp(context, 52));
|
||||
avatar.setLayoutParams(avatarParams);
|
||||
avatar.setGravity(Gravity.CENTER);
|
||||
avatar.setText(firstLetter(firstNonEmpty(row.avatarLabel, row.title, "设")));
|
||||
avatar.setTextSize(22);
|
||||
avatar.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
avatar.setTextColor(resolveDeviceAccentColor(row.statusKey));
|
||||
avatar.setBackground(createRoundedBackground(resolveDeviceAvatarBackground(row.statusKey), dp(context, 18)));
|
||||
avatarWrap.addView(avatar);
|
||||
|
||||
View statusDot = new View(context);
|
||||
FrameLayout.LayoutParams dotParams = new FrameLayout.LayoutParams(dp(context, 10), dp(context, 10), Gravity.END | Gravity.CENTER_VERTICAL);
|
||||
dotParams.rightMargin = dp(context, 1);
|
||||
statusDot.setLayoutParams(dotParams);
|
||||
statusDot.setBackground(createRoundedBackground(resolveDeviceAccentColor(row.statusKey), dp(context, 5)));
|
||||
avatarWrap.addView(statusDot);
|
||||
|
||||
card.addView(avatarWrap);
|
||||
|
||||
LinearLayout textWrap = new LinearLayout(context);
|
||||
textWrap.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
textParams.leftMargin = dp(context, 12);
|
||||
textWrap.setLayoutParams(textParams);
|
||||
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(TextUtils.isEmpty(row.title) ? "设备" : row.title);
|
||||
titleView.setTextSize(17);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
titleView.setMaxLines(1);
|
||||
titleView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
textWrap.addView(titleView);
|
||||
|
||||
TextView subtitleView = new TextView(context);
|
||||
subtitleView.setText(row.subtitle);
|
||||
subtitleView.setTextSize(13);
|
||||
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
subtitleView.setPadding(0, dp(context, 5), 0, 0);
|
||||
subtitleView.setMaxLines(2);
|
||||
subtitleView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
textWrap.addView(subtitleView);
|
||||
|
||||
TextView metaView = new TextView(context);
|
||||
metaView.setText(row.meta);
|
||||
metaView.setTextSize(12);
|
||||
metaView.setTextColor(resolveDeviceMetaColor(context, row.statusKey));
|
||||
metaView.setPadding(0, dp(context, 6), 0, 0);
|
||||
metaView.setMaxLines(2);
|
||||
metaView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
textWrap.addView(metaView);
|
||||
|
||||
card.addView(textWrap);
|
||||
|
||||
if (listener != null) {
|
||||
TextView arrowView = new TextView(context);
|
||||
arrowView.setText("›");
|
||||
arrowView.setTextSize(18);
|
||||
arrowView.setTextColor(context.getColor(R.color.boss_text_soft));
|
||||
arrowView.setPadding(dp(context, 8), 0, 0, 0);
|
||||
card.addView(arrowView);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
public static LinearLayout buildSoftPanel(
|
||||
Context context,
|
||||
String title,
|
||||
String body,
|
||||
@Nullable String meta
|
||||
) {
|
||||
LinearLayout card = new LinearLayout(context);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 12);
|
||||
card.setLayoutParams(params);
|
||||
card.setPadding(dp(context, 16), dp(context, 16), dp(context, 16), dp(context, 16));
|
||||
card.setBackground(createRoundedBackground(Color.parseColor("#E7F5ED"), dp(context, 18)));
|
||||
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(title);
|
||||
titleView.setTextSize(16);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
card.addView(titleView);
|
||||
|
||||
TextView bodyView = new TextView(context);
|
||||
bodyView.setText(body);
|
||||
bodyView.setTextSize(14);
|
||||
bodyView.setLineSpacing(0f, 1.2f);
|
||||
bodyView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
bodyView.setPadding(0, dp(context, 8), 0, 0);
|
||||
card.addView(bodyView);
|
||||
|
||||
if (!TextUtils.isEmpty(meta)) {
|
||||
TextView metaView = new TextView(context);
|
||||
metaView.setText(meta);
|
||||
metaView.setTextSize(12);
|
||||
metaView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
metaView.setPadding(0, dp(context, 10), 0, 0);
|
||||
card.addView(metaView);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
public static LinearLayout buildInlineActionRow(Context context, Button... buttons) {
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
rowParams.leftMargin = dp(context, 12);
|
||||
rowParams.rightMargin = dp(context, 12);
|
||||
rowParams.bottomMargin = dp(context, 12);
|
||||
row.setLayoutParams(rowParams);
|
||||
|
||||
for (int i = 0; i < buttons.length; i++) {
|
||||
Button button = buttons[i];
|
||||
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
if (i > 0) {
|
||||
buttonParams.leftMargin = dp(context, 8);
|
||||
}
|
||||
button.setLayoutParams(buttonParams);
|
||||
row.addView(button);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
public static Button buildMiniActionButton(Context context, String label, boolean primary) {
|
||||
Button button = new Button(context);
|
||||
button.setText(label);
|
||||
button.setAllCaps(false);
|
||||
button.setTextSize(13);
|
||||
button.setPadding(dp(context, 10), dp(context, 10), dp(context, 10), dp(context, 10));
|
||||
button.setTextColor(context.getColor(primary ? R.color.boss_surface : R.color.boss_green));
|
||||
button.setBackgroundResource(primary ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
|
||||
return button;
|
||||
}
|
||||
|
||||
public static LinearLayout buildConversationRow(
|
||||
Context context,
|
||||
WechatSurfaceMapper.ConversationRow row,
|
||||
@Nullable View.OnClickListener listener
|
||||
) {
|
||||
LinearLayout card = new LinearLayout(context);
|
||||
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||
card.setGravity(Gravity.TOP);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 12);
|
||||
card.setLayoutParams(params);
|
||||
card.setPadding(dp(context, 14), dp(context, 14), dp(context, 14), dp(context, 14));
|
||||
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
|
||||
card.setElevation(dp(context, 1));
|
||||
if (listener != null) {
|
||||
card.setClickable(true);
|
||||
card.setFocusable(true);
|
||||
card.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
card.addView(buildConversationAvatar(context, row));
|
||||
|
||||
LinearLayout centerColumn = new LinearLayout(context);
|
||||
centerColumn.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams centerParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
centerParams.leftMargin = dp(context, 12);
|
||||
centerParams.rightMargin = dp(context, 8);
|
||||
centerColumn.setLayoutParams(centerParams);
|
||||
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(TextUtils.isEmpty(row.threadTitle) ? "未命名会话" : row.threadTitle);
|
||||
titleView.setTextSize(18);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
titleView.setMaxLines(1);
|
||||
titleView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
centerColumn.addView(titleView);
|
||||
|
||||
if (!TextUtils.isEmpty(row.folderLabel)) {
|
||||
TextView folderView = new TextView(context);
|
||||
folderView.setText(row.folderLabel);
|
||||
folderView.setTextSize(13);
|
||||
folderView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
folderView.setPadding(0, dp(context, 4), 0, 0);
|
||||
folderView.setMaxLines(1);
|
||||
folderView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
centerColumn.addView(folderView);
|
||||
}
|
||||
|
||||
TextView previewView = new TextView(context);
|
||||
previewView.setText(TextUtils.isEmpty(row.lastMessagePreview) ? "暂无消息" : row.lastMessagePreview);
|
||||
previewView.setTextSize(14);
|
||||
previewView.setTextColor(context.getColor(R.color.boss_text_soft));
|
||||
previewView.setPadding(0, dp(context, 5), 0, 0);
|
||||
previewView.setMaxLines(2);
|
||||
previewView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
centerColumn.addView(previewView);
|
||||
|
||||
card.addView(centerColumn);
|
||||
|
||||
LinearLayout trailingColumn = new LinearLayout(context);
|
||||
trailingColumn.setOrientation(LinearLayout.VERTICAL);
|
||||
trailingColumn.setGravity(Gravity.END);
|
||||
trailingColumn.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
if (!TextUtils.isEmpty(row.topPinnedLabel)) {
|
||||
TextView pinnedView = new TextView(context);
|
||||
pinnedView.setText(row.topPinnedLabel);
|
||||
pinnedView.setTextSize(11);
|
||||
pinnedView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
pinnedView.setTextColor(context.getColor(R.color.boss_green));
|
||||
pinnedView.setBackground(createRoundedBackground(Color.parseColor("#E7F7EE"), dp(context, 9)));
|
||||
pinnedView.setPadding(dp(context, 7), dp(context, 3), dp(context, 7), dp(context, 3));
|
||||
trailingColumn.addView(pinnedView);
|
||||
}
|
||||
|
||||
TextView timeView = new TextView(context);
|
||||
timeView.setText(TextUtils.isEmpty(row.timeLabel) ? "--:--" : row.timeLabel);
|
||||
timeView.setTextSize(12);
|
||||
timeView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
timeView.setPadding(0, dp(context, TextUtils.isEmpty(row.topPinnedLabel) ? 2 : 8), 0, 0);
|
||||
trailingColumn.addView(timeView);
|
||||
|
||||
if (row.unreadCount > 0) {
|
||||
TextView unreadView = new TextView(context);
|
||||
unreadView.setText(row.unreadCount > 99 ? "99+" : String.valueOf(row.unreadCount));
|
||||
unreadView.setTextSize(11);
|
||||
unreadView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
unreadView.setTextColor(Color.WHITE);
|
||||
unreadView.setGravity(Gravity.CENTER);
|
||||
unreadView.setMinWidth(dp(context, 20));
|
||||
unreadView.setBackground(createRoundedBackground(Color.parseColor("#FF5A5A"), dp(context, 10)));
|
||||
unreadView.setPadding(dp(context, 6), dp(context, 2), dp(context, 6), dp(context, 2));
|
||||
LinearLayout.LayoutParams unreadParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
unreadParams.topMargin = dp(context, 8);
|
||||
unreadView.setLayoutParams(unreadParams);
|
||||
trailingColumn.addView(unreadView);
|
||||
}
|
||||
|
||||
View spacer = new View(context);
|
||||
spacer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
0,
|
||||
1f
|
||||
));
|
||||
trailingColumn.addView(spacer);
|
||||
|
||||
LinearLayout activityWrap = new LinearLayout(context);
|
||||
activityWrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
activityWrap.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams activityParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
activityParams.topMargin = dp(context, 10);
|
||||
activityWrap.setLayoutParams(activityParams);
|
||||
int activityCount = Math.max(0, Math.min(row.activityIconCount, WechatSurfaceMapper.maxConversationActivityIcons()));
|
||||
for (int i = 0; i < activityCount; i++) {
|
||||
View dot = buildAnimatedActivityDot(context, i);
|
||||
if (i > 0) {
|
||||
LinearLayout.LayoutParams dotParams = (LinearLayout.LayoutParams) dot.getLayoutParams();
|
||||
dotParams.leftMargin = dp(context, 4);
|
||||
dot.setLayoutParams(dotParams);
|
||||
}
|
||||
activityWrap.addView(dot);
|
||||
}
|
||||
trailingColumn.addView(activityWrap);
|
||||
|
||||
card.addView(trailingColumn);
|
||||
return card;
|
||||
}
|
||||
|
||||
public static LinearLayout buildEmptyCard(Context context, String text) {
|
||||
return buildCard(context, "暂无内容", text, "下拉或点击顶部刷新按钮重试。");
|
||||
}
|
||||
|
||||
public static TextView buildHintPill(Context context, String text) {
|
||||
TextView pill = new TextView(context);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.leftMargin = dp(context, 12);
|
||||
params.rightMargin = dp(context, 12);
|
||||
params.bottomMargin = dp(context, 12);
|
||||
pill.setLayoutParams(params);
|
||||
pill.setText(text);
|
||||
pill.setTextSize(12);
|
||||
pill.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
pill.setPadding(dp(context, 12), dp(context, 7), dp(context, 12), dp(context, 7));
|
||||
pill.setBackground(createRoundedBackground(Color.parseColor("#F4F5F2"), dp(context, 14)));
|
||||
return pill;
|
||||
}
|
||||
|
||||
public static LinearLayout buildMessageBubble(
|
||||
Context context,
|
||||
String senderLabel,
|
||||
@@ -312,4 +806,202 @@ public final class BossUi {
|
||||
public static int dp(Context context, int value) {
|
||||
return Math.round(value * context.getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private static View buildConversationAvatar(Context context, WechatSurfaceMapper.ConversationRow row) {
|
||||
if (!row.isGroup) {
|
||||
return buildAvatarCircle(
|
||||
context,
|
||||
firstNonEmpty(row.avatarPrimary, row.threadTitle, "会"),
|
||||
AVATAR_BG_COLORS[0],
|
||||
Color.WHITE,
|
||||
52
|
||||
);
|
||||
}
|
||||
|
||||
FrameLayout groupWrap = new FrameLayout(context);
|
||||
LinearLayout.LayoutParams wrapParams = new LinearLayout.LayoutParams(dp(context, 52), dp(context, 52));
|
||||
groupWrap.setLayoutParams(wrapParams);
|
||||
|
||||
GradientDrawable bg = createRoundedBackground(Color.parseColor("#F2F6F3"), dp(context, 18));
|
||||
bg.setStroke(dp(context, 1), Color.parseColor("#E5ECE7"));
|
||||
groupWrap.setBackground(bg);
|
||||
|
||||
int visibleCount = Math.min(row.groupAvatarMembers.length, 4);
|
||||
if (visibleCount == 0) {
|
||||
groupWrap.addView(buildCenteredAvatarTile(context, "群", AVATAR_BG_COLORS[1], 32, 10, 10));
|
||||
return groupWrap;
|
||||
}
|
||||
|
||||
int[][] offsets = {
|
||||
{4, 4},
|
||||
{24, 4},
|
||||
{4, 24},
|
||||
{24, 24}
|
||||
};
|
||||
for (int i = 0; i < visibleCount; i++) {
|
||||
WechatSurfaceMapper.GroupAvatarMember member = row.groupAvatarMembers[i];
|
||||
groupWrap.addView(buildCenteredAvatarTile(
|
||||
context,
|
||||
firstNonEmpty(member.avatarLabel, member.title, "群"),
|
||||
AVATAR_BG_COLORS[(i + 1) % AVATAR_BG_COLORS.length],
|
||||
22,
|
||||
offsets[i][0],
|
||||
offsets[i][1]
|
||||
));
|
||||
}
|
||||
return groupWrap;
|
||||
}
|
||||
|
||||
private static TextView buildAvatarCircle(
|
||||
Context context,
|
||||
String labelSource,
|
||||
int backgroundColor,
|
||||
int textColor,
|
||||
int sizeDp
|
||||
) {
|
||||
TextView avatarView = new TextView(context);
|
||||
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, sizeDp), dp(context, sizeDp));
|
||||
avatarView.setLayoutParams(avatarParams);
|
||||
avatarView.setGravity(Gravity.CENTER);
|
||||
avatarView.setText(firstLetter(labelSource));
|
||||
avatarView.setTextSize(sizeDp >= 48 ? 24 : 12);
|
||||
avatarView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
avatarView.setTextColor(textColor);
|
||||
avatarView.setBackground(createRoundedBackground(backgroundColor, dp(context, sizeDp / 2)));
|
||||
return avatarView;
|
||||
}
|
||||
|
||||
private static View buildCenteredAvatarTile(
|
||||
Context context,
|
||||
String labelSource,
|
||||
int backgroundColor,
|
||||
int sizeDp,
|
||||
int leftDp,
|
||||
int topDp
|
||||
) {
|
||||
TextView tile = buildAvatarCircle(context, labelSource, backgroundColor, context.getColor(R.color.boss_text_primary), sizeDp);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dp(context, sizeDp), dp(context, sizeDp));
|
||||
params.leftMargin = dp(context, leftDp);
|
||||
params.topMargin = dp(context, topDp);
|
||||
tile.setLayoutParams(params);
|
||||
return tile;
|
||||
}
|
||||
|
||||
private static GradientDrawable createRoundedBackground(int color, int radiusPx) {
|
||||
GradientDrawable drawable = new GradientDrawable();
|
||||
drawable.setColor(color);
|
||||
drawable.setCornerRadius(radiusPx);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private static int resolveDeviceAccentColor(String statusKey) {
|
||||
if ("online".equals(statusKey)) {
|
||||
return DEVICE_STATUS_ONLINE;
|
||||
}
|
||||
if ("abnormal".equals(statusKey)) {
|
||||
return DEVICE_STATUS_ABNORMAL;
|
||||
}
|
||||
return DEVICE_STATUS_OFFLINE;
|
||||
}
|
||||
|
||||
private static int resolveDeviceAvatarBackground(String statusKey) {
|
||||
if ("online".equals(statusKey)) {
|
||||
return Color.parseColor("#E5F6EC");
|
||||
}
|
||||
if ("abnormal".equals(statusKey)) {
|
||||
return Color.parseColor("#FCE9E7");
|
||||
}
|
||||
return Color.parseColor("#F2F4F7");
|
||||
}
|
||||
|
||||
private static int resolveDeviceMetaColor(Context context, String statusKey) {
|
||||
if ("online".equals(statusKey)) {
|
||||
return context.getColor(R.color.boss_text_muted);
|
||||
}
|
||||
return resolveDeviceAccentColor(statusKey);
|
||||
}
|
||||
|
||||
private static View buildAnimatedActivityDot(Context context, int index) {
|
||||
View dot = new View(context);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dp(context, 7), dp(context, 7));
|
||||
dot.setLayoutParams(params);
|
||||
|
||||
GradientDrawable drawable = createRoundedBackground(
|
||||
ACTIVITY_ICON_COLORS[index % ACTIVITY_ICON_COLORS.length],
|
||||
dp(context, 4)
|
||||
);
|
||||
dot.setBackground(drawable);
|
||||
dot.setAlpha(0.35f);
|
||||
dot.setScaleX(0.82f);
|
||||
dot.setScaleY(0.82f);
|
||||
|
||||
final AnimatorSet[] animatorRef = new AnimatorSet[1];
|
||||
dot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
cancelConversationActivityAnimator(animatorRef);
|
||||
animatorRef[0] = createConversationActivityAnimator(v, index);
|
||||
animatorRef[0].start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
cancelConversationActivityAnimator(animatorRef);
|
||||
v.animate().cancel();
|
||||
v.setAlpha(0.35f);
|
||||
v.setScaleX(0.82f);
|
||||
v.setScaleY(0.82f);
|
||||
}
|
||||
});
|
||||
return dot;
|
||||
}
|
||||
|
||||
private static AnimatorSet createConversationActivityAnimator(View dot, int index) {
|
||||
long durationMs = 850L;
|
||||
long startDelayMs = index * 120L;
|
||||
ObjectAnimator alpha = ObjectAnimator.ofFloat(dot, View.ALPHA, 0.35f, 1f, 0.35f);
|
||||
alpha.setDuration(durationMs);
|
||||
alpha.setStartDelay(startDelayMs);
|
||||
alpha.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
|
||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(dot, View.SCALE_X, 0.82f, 1.1f, 0.82f);
|
||||
scaleX.setDuration(durationMs);
|
||||
scaleX.setStartDelay(startDelayMs);
|
||||
scaleX.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
|
||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(dot, View.SCALE_Y, 0.82f, 1.1f, 0.82f);
|
||||
scaleY.setDuration(durationMs);
|
||||
scaleY.setStartDelay(startDelayMs);
|
||||
scaleY.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
|
||||
AnimatorSet pulse = new AnimatorSet();
|
||||
pulse.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
pulse.playTogether(alpha, scaleX, scaleY);
|
||||
return pulse;
|
||||
}
|
||||
|
||||
private static void cancelConversationActivityAnimator(AnimatorSet[] animatorRef) {
|
||||
if (animatorRef[0] != null) {
|
||||
animatorRef[0].cancel();
|
||||
animatorRef[0] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstLetter(String value) {
|
||||
String text = value == null ? "" : value.trim();
|
||||
if (text.isEmpty()) {
|
||||
return "会";
|
||||
}
|
||||
return text.substring(0, 1);
|
||||
}
|
||||
|
||||
private static String firstNonEmpty(String first, String second, String fallback) {
|
||||
if (!TextUtils.isEmpty(first)) {
|
||||
return first;
|
||||
}
|
||||
if (!TextUtils.isEmpty(second)) {
|
||||
return second;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class ConversationInfoActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
private String projectFolderName;
|
||||
private int participantCount;
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.activity_conversation_info;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("会话信息", projectName == null ? "单线程会话信息页" : projectName);
|
||||
setHeaderAction("重命名", v -> openRenameDialog());
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
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());
|
||||
runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
replaceContent(BossUi.buildEmptyCard(this, "会话信息加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderConversation(JSONObject detail, JSONObject participantsPayload) {
|
||||
replaceContent();
|
||||
JSONObject project = detail.optJSONObject("project");
|
||||
JSONArray participants = participantsPayload.optJSONArray("participants");
|
||||
|
||||
if (project == null) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "会话不存在。"));
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
projectName = project.optString("name", projectName == null ? "会话信息" : projectName);
|
||||
JSONObject threadMeta = project.optJSONObject("threadMeta");
|
||||
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
participantCount = participants == null ? 0 : participants.length();
|
||||
configureScreen("会话信息", buildSubtitle(threadMeta, participantCount));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
projectName,
|
||||
buildDetailBody(project, threadMeta),
|
||||
buildDetailMeta(projectId, projectFolderName, participantCount)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildMenuRow(
|
||||
this,
|
||||
"发起群聊",
|
||||
"从当前会话选择其他线程,创建新的独立群聊",
|
||||
null,
|
||||
v -> openGroupCreate()
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"参与设备 / 线程",
|
||||
"以下线程参与当前会话,点击可查看对应项目详情。",
|
||||
participantCount == 0 ? "当前没有可展示的参与线程。" : "共 " + participantCount + " 个参与线程"
|
||||
));
|
||||
|
||||
if (participants == null || participants.length() == 0) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "当前没有参与线程信息。"));
|
||||
} else {
|
||||
for (int i = 0; i < participants.length(); i++) {
|
||||
JSONObject participant = participants.optJSONObject(i);
|
||||
if (participant == null) continue;
|
||||
appendContent(buildParticipantRow(participant));
|
||||
}
|
||||
}
|
||||
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildParticipantRow(JSONObject participant) {
|
||||
boolean sourceProject = participant.optBoolean("isSourceProject", false);
|
||||
String participantProjectId = participant.optString("projectId", "");
|
||||
String title = participant.optString("threadDisplayName", "未命名线程");
|
||||
String subtitle = participant.optString("folderName", "");
|
||||
String meta = participant.optString("deviceId", "");
|
||||
if (!participant.optString("threadId", "").isEmpty()) {
|
||||
meta = meta.isEmpty() ? participant.optString("threadId", "") : meta + " · " + participant.optString("threadId", "");
|
||||
}
|
||||
return BossUi.buildListRow(
|
||||
this,
|
||||
title,
|
||||
subtitle,
|
||||
meta,
|
||||
sourceProject ? "来源" : null,
|
||||
v -> openProject(participantProjectId, title)
|
||||
);
|
||||
}
|
||||
|
||||
private void openProject(String targetProjectId, String targetProjectName) {
|
||||
if (targetProjectId == null || targetProjectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, ProjectDetailActivity.class);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, targetProjectId);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, targetProjectName);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void openGroupCreate() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, GroupCreateActivity.class);
|
||||
intent.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_ID, projectId);
|
||||
intent.putExtra(GroupCreateActivity.EXTRA_SOURCE_PROJECT_NAME, projectName);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void openRenameDialog() {
|
||||
final EditText input = BossUi.buildInput(this, "线程名", false);
|
||||
input.setText(projectName == null ? "" : projectName);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("重命名会话")
|
||||
.setView(input)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveConversationName(input.getText().toString().trim()))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void saveConversationName(String name) {
|
||||
if (name.isEmpty()) {
|
||||
showMessage("线程名不能为空");
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.renameConversation(projectId, name, false);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(EXTRA_PROJECT_NAME, name);
|
||||
setResult(RESULT_OK, result);
|
||||
showMessage("线程名已更新");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String buildSubtitle(@Nullable JSONObject threadMeta, int count) {
|
||||
String folder = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
String suffix = count <= 0 ? "暂无参与线程" : count + " 个参与线程";
|
||||
if (folder.isEmpty()) {
|
||||
return suffix;
|
||||
}
|
||||
return folder + " · " + suffix;
|
||||
}
|
||||
|
||||
private String buildDetailBody(JSONObject project, @Nullable JSONObject threadMeta) {
|
||||
String threadId = threadMeta == null ? project.optString("id", "") : threadMeta.optString("threadId", "");
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
String deviceCount = project.optJSONArray("deviceIds") == null ? "0" : String.valueOf(project.optJSONArray("deviceIds").length());
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("线程 ID:").append(threadId.isEmpty() ? project.optString("id", "-") : threadId);
|
||||
builder.append("\n文件夹:").append(folderName.isEmpty() ? "未命名文件夹" : folderName);
|
||||
builder.append("\n绑定设备:").append(deviceCount);
|
||||
builder.append("\n群聊状态:").append(project.optBoolean("isGroup", false) ? "群聊" : "单线程");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String buildDetailMeta(String projectId, String folderName, int count) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (!projectId.isEmpty()) {
|
||||
builder.append("project ").append(projectId);
|
||||
}
|
||||
if (!folderName.isEmpty()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(folderName);
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(count <= 0 ? "暂无参与线程" : "参与线程 " + count);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
|
||||
deviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
|
||||
configureScreen(deviceName == null ? "设备详情" : deviceName, "原生设备详情");
|
||||
hideHeaderAction();
|
||||
configureScreen(deviceName == null ? "设备详情" : deviceName, "设备状态与绑定项目");
|
||||
setHeaderAction("编辑", v -> openEditDialog());
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -56,19 +56,25 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
deviceName = device.optString("name", deviceId);
|
||||
configureScreen(deviceName, device.optString("endpoint", "设备详情"));
|
||||
configureScreen(deviceName, "设备状态与绑定项目");
|
||||
WechatSurfaceMapper.DeviceDetailSummary summary = WechatSurfaceMapper.toDeviceDetailSummary(device);
|
||||
appendContent(BossUi.buildListRow(
|
||||
appendContent(BossUi.buildDeviceCard(
|
||||
this,
|
||||
summary.title.isEmpty() ? "设备" : summary.title,
|
||||
summary.subtitle,
|
||||
summary.meta,
|
||||
WechatSurfaceMapper.toDeviceRow(device),
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
if (summary.meta != null && !summary.meta.isEmpty()) {
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"设备说明",
|
||||
summary.meta,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
appendContent(BossUi.buildMenuRow(this, "查看技能", "查看当前设备同步的 Skill 清单", null, v -> openSkills()));
|
||||
appendContent(BossUi.buildMenuRow(this, "编辑", "修改设备名称、备注和项目列表", null, v -> openEditDialog()));
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
configureScreen("添加设备", "通过 pairing code 或 token 把新设备接入");
|
||||
configureScreen("添加设备", "填写设备信息后生成配对草稿");
|
||||
hideHeaderAction();
|
||||
buildForm();
|
||||
}
|
||||
@@ -38,23 +38,24 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
|
||||
noteInput = BossUi.buildInput(this, "备注", true);
|
||||
projectsInput = BossUi.buildInput(this, "项目列表,逗号分隔", true);
|
||||
|
||||
android.widget.Button submitButton = BossUi.buildPrimaryButton(this, "生成绑定草稿");
|
||||
submitButton.setOnClickListener(v -> submitEnrollment());
|
||||
|
||||
replaceContent(
|
||||
BossUi.buildCard(
|
||||
BossUi.buildSoftPanel(
|
||||
this,
|
||||
"绑定新设备",
|
||||
"支持通过 pairing code、临时 token 或登录引导把 Mac、Windows、云端节点接入。",
|
||||
"当前原生页会直接调用 /api/v1/devices/enrollments"
|
||||
"接入新设备",
|
||||
"支持通过 pairing code 或 token 接入 Mac、Windows、云端节点。",
|
||||
"生成后把配对码交给设备端即可完成绑定。"
|
||||
),
|
||||
nameInput,
|
||||
avatarInput,
|
||||
accountInput,
|
||||
endpointInput,
|
||||
noteInput,
|
||||
projectsInput,
|
||||
BossUi.buildPrimaryButton(this, "生成绑定草稿")
|
||||
BossUi.buildFormCell(this, "设备名称", "例如 Mac Studio 或 Windows GPU", nameInput),
|
||||
BossUi.buildFormCell(this, "头像字符", "会显示在设备卡片左侧", avatarInput),
|
||||
BossUi.buildFormCell(this, "所属账号", "默认使用当前登录账号", accountInput),
|
||||
BossUi.buildFormCell(this, "设备地址", "例如 mac://kris.local", endpointInput),
|
||||
BossUi.buildFormCell(this, "设备备注", "可填写位置、用途或节点说明", noteInput),
|
||||
BossUi.buildFormCell(this, "项目列表", "多个项目用逗号分隔", projectsInput),
|
||||
submitButton
|
||||
);
|
||||
((android.widget.Button) contentLayout.getChildAt(contentLayout.getChildCount() - 1))
|
||||
.setOnClickListener(v -> submitEnrollment());
|
||||
}
|
||||
|
||||
private void submitEnrollment() {
|
||||
@@ -80,7 +81,7 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
|
||||
JSONObject enrollment = response.json.optJSONObject("enrollment");
|
||||
JSONObject device = response.json.optJSONObject("device");
|
||||
replaceContent(
|
||||
BossUi.buildCard(
|
||||
BossUi.buildSoftPanel(
|
||||
this,
|
||||
"绑定草稿已生成",
|
||||
"设备 " + (device == null ? "-" : device.optString("name", "-"))
|
||||
|
||||
356
android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java
Normal file
356
android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java
Normal file
@@ -0,0 +1,356 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class GroupCreateActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_SOURCE_PROJECT_ID = "source_project_id";
|
||||
public static final String EXTRA_SOURCE_PROJECT_NAME = "source_project_name";
|
||||
|
||||
private final List<CandidateConversation> candidates = new ArrayList<>();
|
||||
private final Set<String> selectedProjectIds = new LinkedHashSet<>();
|
||||
private final Set<String> lastCandidateProjectIds = new LinkedHashSet<>();
|
||||
|
||||
private String sourceProjectId;
|
||||
private String sourceProjectName;
|
||||
private String sourceFolderName;
|
||||
private LinearLayout candidateListLayout;
|
||||
private Button createButton;
|
||||
private boolean creatingGroupChat;
|
||||
private JSONObject cachedParticipantsPayload;
|
||||
private JSONObject cachedConversationsPayload;
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.activity_group_create;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
sourceProjectId = getIntent().getStringExtra(EXTRA_SOURCE_PROJECT_ID);
|
||||
sourceProjectName = getIntent().getStringExtra(EXTRA_SOURCE_PROJECT_NAME);
|
||||
configureScreen("发起群聊", sourceProjectName == null ? "从当前会话出发" : sourceProjectName);
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (sourceProjectId == null || sourceProjectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(sourceProjectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
BossApiClient.ApiResponse conversationsResponse = apiClient.getConversations();
|
||||
if (!conversationsResponse.ok()) throw new IllegalStateException(conversationsResponse.message());
|
||||
runOnUiThread(() -> renderCreatePage(participantsResponse.json, conversationsResponse.json, true));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
replaceContent(BossUi.buildEmptyCard(this, "群聊创建页加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderCreatePage(JSONObject participantsPayload, JSONObject conversationsPayload, boolean rebuildCandidates) {
|
||||
cachedParticipantsPayload = participantsPayload;
|
||||
cachedConversationsPayload = conversationsPayload;
|
||||
replaceContent();
|
||||
|
||||
JSONObject threadMeta = participantsPayload.optJSONObject("threadMeta");
|
||||
JSONArray participants = participantsPayload.optJSONArray("participants");
|
||||
sourceFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
sourceProjectName = threadMeta == null
|
||||
? sourceProjectName
|
||||
: threadMeta.optString("threadDisplayName", sourceProjectName == null ? "当前会话" : sourceProjectName);
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"新建独立群聊",
|
||||
"群聊不是升级原会话,而是以当前会话为源,新建一个独立线程。",
|
||||
buildSourceMeta(threadMeta, participants)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
sourceProjectName,
|
||||
buildSourceBody(threadMeta, participants),
|
||||
sourceProjectId + (sourceFolderName.isEmpty() ? "" : " · " + sourceFolderName)
|
||||
));
|
||||
|
||||
if (rebuildCandidates) {
|
||||
List<JSONObject> selectableConversations = collectSelectableConversationItems(conversationsPayload, sourceProjectId);
|
||||
List<CandidateConversation> nextCandidates = new ArrayList<>(selectableConversations.size());
|
||||
Set<String> nextCandidateProjectIds = new LinkedHashSet<>();
|
||||
for (JSONObject item : selectableConversations) {
|
||||
CandidateConversation candidate = new CandidateConversation(
|
||||
item.optString("projectId", ""),
|
||||
item.optString("projectTitle", item.optString("threadTitle", "未命名会话")),
|
||||
item.optString("folderLabel", ""),
|
||||
item.optString("lastMessagePreview", item.optString("preview", "")),
|
||||
item.optString("latestReplyLabel", ""),
|
||||
false
|
||||
);
|
||||
nextCandidates.add(candidate);
|
||||
nextCandidateProjectIds.add(candidate.projectId);
|
||||
}
|
||||
Set<String> currentSelectedProjectIds = new LinkedHashSet<>(selectedProjectIds);
|
||||
candidates.clear();
|
||||
candidates.addAll(nextCandidates);
|
||||
selectedProjectIds.clear();
|
||||
selectedProjectIds.addAll(reconcileSelectedProjectIds(
|
||||
currentSelectedProjectIds,
|
||||
lastCandidateProjectIds,
|
||||
nextCandidateProjectIds
|
||||
));
|
||||
lastCandidateProjectIds.clear();
|
||||
lastCandidateProjectIds.addAll(nextCandidateProjectIds);
|
||||
}
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"选择其他线程",
|
||||
candidates.isEmpty()
|
||||
? "当前没有可加入的其他线程。"
|
||||
: selectedProjectIds.isEmpty()
|
||||
? "你已取消全部勾选,可继续手动选择。"
|
||||
: "已保留你当前的勾选状态。",
|
||||
"已选 " + selectedProjectIds.size() + " 个线程"
|
||||
));
|
||||
|
||||
candidateListLayout = new LinearLayout(this);
|
||||
candidateListLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
for (CandidateConversation candidate : candidates) {
|
||||
candidateListLayout.addView(buildCandidateRow(candidate));
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
candidateListLayout.addView(BossUi.buildEmptyCard(this, "当前没有可选择的其他线程。"));
|
||||
}
|
||||
appendContent(candidateListLayout);
|
||||
|
||||
createButton = BossUi.buildPrimaryButton(this, "创建群聊");
|
||||
createButton.setOnClickListener(v -> createGroupChat());
|
||||
appendContent(createButton);
|
||||
|
||||
Button cancelButton = BossUi.buildSecondaryButton(this, "取消");
|
||||
cancelButton.setOnClickListener(v -> finish());
|
||||
appendContent(cancelButton);
|
||||
|
||||
setRefreshing(false);
|
||||
updateCreateButtonState();
|
||||
}
|
||||
|
||||
static List<JSONObject> collectSelectableConversationItems(@Nullable JSONObject conversationsPayload, String sourceProjectId) {
|
||||
List<JSONObject> result = new ArrayList<>();
|
||||
JSONArray conversations = conversationsPayload == null ? null : conversationsPayload.optJSONArray("conversations");
|
||||
if (conversations == null) {
|
||||
return result;
|
||||
}
|
||||
for (int i = 0; i < conversations.length(); i++) {
|
||||
JSONObject item = conversations.optJSONObject(i);
|
||||
if (item == null) continue;
|
||||
String projectId = item.optString("projectId", "");
|
||||
if (projectId.isEmpty() || sourceProjectId.equals(projectId) || item.optBoolean("isGroup", false)) {
|
||||
continue;
|
||||
}
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private LinearLayout buildCandidateRow(CandidateConversation candidate) {
|
||||
boolean selected = selectedProjectIds.contains(candidate.projectId);
|
||||
String badge = selected ? "已选" : "未选";
|
||||
String subtitle = candidate.folderLabel.isEmpty() ? candidate.latestReplyLabel : candidate.folderLabel;
|
||||
String meta = candidate.preview;
|
||||
if (!candidate.latestReplyLabel.isEmpty() && !candidate.latestReplyLabel.equals(candidate.preview)) {
|
||||
meta = candidate.latestReplyLabel + (meta.isEmpty() ? "" : " · " + meta);
|
||||
}
|
||||
return BossUi.buildListRow(
|
||||
this,
|
||||
candidate.title,
|
||||
subtitle,
|
||||
meta,
|
||||
badge,
|
||||
v -> toggleSelection(candidate.projectId)
|
||||
);
|
||||
}
|
||||
|
||||
private void toggleSelection(String projectId) {
|
||||
if (selectedProjectIds.contains(projectId)) {
|
||||
selectedProjectIds.remove(projectId);
|
||||
} else {
|
||||
selectedProjectIds.add(projectId);
|
||||
}
|
||||
refreshCandidateRows();
|
||||
updateCreateButtonState();
|
||||
}
|
||||
|
||||
private void refreshCandidateRows() {
|
||||
if (cachedParticipantsPayload == null || cachedConversationsPayload == null) {
|
||||
return;
|
||||
}
|
||||
renderCreatePage(cachedParticipantsPayload, cachedConversationsPayload, false);
|
||||
}
|
||||
|
||||
private void updateCreateButtonState() {
|
||||
if (createButton != null) {
|
||||
boolean refreshing = refreshLayout != null && refreshLayout.isRefreshing();
|
||||
createButton.setEnabled(canCreateGroupChat(refreshing, creatingGroupChat, selectedProjectIds));
|
||||
createButton.setText(creatingGroupChat ? "创建中..." : "创建群聊");
|
||||
}
|
||||
}
|
||||
|
||||
private void createGroupChat() {
|
||||
boolean refreshing = refreshLayout != null && refreshLayout.isRefreshing();
|
||||
if (refreshing || creatingGroupChat) {
|
||||
return;
|
||||
}
|
||||
if (selectedProjectIds.isEmpty()) {
|
||||
showMessage("请至少选择一个其他线程");
|
||||
return;
|
||||
}
|
||||
List<String> memberProjectIdsSnapshot = new ArrayList<>(selectedProjectIds);
|
||||
creatingGroupChat = true;
|
||||
setRefreshing(true);
|
||||
updateCreateButtonState();
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
JSONArray memberProjectIds = new JSONArray();
|
||||
for (String projectId : memberProjectIdsSnapshot) {
|
||||
memberProjectIds.put(projectId);
|
||||
}
|
||||
payload.put("memberProjectIds", memberProjectIds);
|
||||
BossApiClient.ApiResponse response = apiClient.createGroupChat(sourceProjectId, payload);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
JSONObject project = response.json.optJSONObject("project");
|
||||
if (project == null) throw new IllegalStateException("GROUP_CHAT_PROJECT_MISSING");
|
||||
String createdProjectId = project.optString("id", "");
|
||||
if (createdProjectId.isEmpty()) {
|
||||
throw new IllegalStateException("GROUP_CHAT_PROJECT_ID_MISSING");
|
||||
}
|
||||
String createdProjectName = project.optString("name", sourceProjectName == null ? "群聊" : sourceProjectName);
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
creatingGroupChat = false;
|
||||
updateCreateButtonState();
|
||||
showMessage("群聊已创建");
|
||||
Intent intent = new Intent(this, ProjectDetailActivity.class);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, createdProjectId);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, createdProjectName);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
creatingGroupChat = false;
|
||||
setRefreshing(false);
|
||||
showMessage("创建失败:" + error.getMessage());
|
||||
updateCreateButtonState();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static boolean canCreateGroupChat(
|
||||
boolean refreshing,
|
||||
boolean creatingGroupChat,
|
||||
@Nullable Set<String> selectedProjectIds
|
||||
) {
|
||||
return !refreshing
|
||||
&& !creatingGroupChat
|
||||
&& selectedProjectIds != null
|
||||
&& !selectedProjectIds.isEmpty();
|
||||
}
|
||||
|
||||
static Set<String> reconcileSelectedProjectIds(
|
||||
@Nullable Set<String> currentSelectedProjectIds,
|
||||
@Nullable Set<String> previousCandidateProjectIds,
|
||||
@Nullable Set<String> nextCandidateProjectIds
|
||||
) {
|
||||
Set<String> reconciled = new LinkedHashSet<>();
|
||||
if (nextCandidateProjectIds == null || nextCandidateProjectIds.isEmpty()) {
|
||||
return reconciled;
|
||||
}
|
||||
if (previousCandidateProjectIds == null
|
||||
|| previousCandidateProjectIds.isEmpty()
|
||||
|| !previousCandidateProjectIds.equals(nextCandidateProjectIds)) {
|
||||
reconciled.addAll(nextCandidateProjectIds);
|
||||
return reconciled;
|
||||
}
|
||||
if (currentSelectedProjectIds == null || currentSelectedProjectIds.isEmpty()) {
|
||||
return reconciled;
|
||||
}
|
||||
for (String projectId : currentSelectedProjectIds) {
|
||||
if (nextCandidateProjectIds.contains(projectId)) {
|
||||
reconciled.add(projectId);
|
||||
}
|
||||
}
|
||||
return reconciled;
|
||||
}
|
||||
|
||||
private String buildSourceMeta(@Nullable JSONObject threadMeta, @Nullable JSONArray participants) {
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
int count = participants == null ? 0 : participants.length();
|
||||
String memberLabel = count <= 0 ? "暂无参与线程" : count + " 个参与线程";
|
||||
if (folderName.isEmpty()) {
|
||||
return memberLabel;
|
||||
}
|
||||
return folderName + " · " + memberLabel;
|
||||
}
|
||||
|
||||
private String buildSourceBody(@Nullable JSONObject threadMeta, @Nullable JSONArray participants) {
|
||||
String threadId = threadMeta == null ? sourceProjectId : threadMeta.optString("threadId", sourceProjectId);
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("来源线程:").append(threadId);
|
||||
builder.append("\n文件夹:").append(folderName.isEmpty() ? "未命名文件夹" : folderName);
|
||||
builder.append("\n参与线程:").append(participants == null ? 0 : participants.length());
|
||||
builder.append("\n默认规则:会自动勾选当前会话之外的其他线程");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final class CandidateConversation {
|
||||
private final String projectId;
|
||||
private final String title;
|
||||
private final String folderLabel;
|
||||
private final String preview;
|
||||
private final String latestReplyLabel;
|
||||
private final boolean isGroup;
|
||||
|
||||
private CandidateConversation(
|
||||
String projectId,
|
||||
String title,
|
||||
String folderLabel,
|
||||
String preview,
|
||||
String latestReplyLabel,
|
||||
boolean isGroup
|
||||
) {
|
||||
this.projectId = projectId;
|
||||
this.title = title;
|
||||
this.folderLabel = folderLabel;
|
||||
this.preview = preview;
|
||||
this.latestReplyLabel = latestReplyLabel;
|
||||
this.isGroup = isGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java
Normal file
208
android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java
Normal file
@@ -0,0 +1,208 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class GroupInfoActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.activity_group_info;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("群资料", projectName == null ? "群聊资料页" : projectName);
|
||||
setHeaderAction("重命名", v -> openRenameDialog());
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
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());
|
||||
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
replaceContent(BossUi.buildEmptyCard(this, "群资料加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderGroup(JSONObject detail, JSONObject participantsPayload) {
|
||||
replaceContent();
|
||||
JSONObject project = detail.optJSONObject("project");
|
||||
JSONArray participants = participantsPayload.optJSONArray("participants");
|
||||
|
||||
if (project == null) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "群聊不存在。"));
|
||||
setRefreshing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
projectName = project.optString("name", projectName == null ? "群聊" : projectName);
|
||||
JSONObject threadMeta = project.optJSONObject("threadMeta");
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
int participantCount = participants == null ? 0 : participants.length();
|
||||
configureScreen("群资料", buildSubtitle(folderName, participantCount));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
projectName,
|
||||
buildDetailBody(project, threadMeta),
|
||||
buildDetailMeta(projectId, folderName, participantCount)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"成员线程",
|
||||
"群聊成员可点击查看对应项目详情。",
|
||||
participantCount == 0 ? "当前没有成员线程。" : "共 " + participantCount + " 个成员"
|
||||
));
|
||||
|
||||
if (participants == null || participants.length() == 0) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "当前没有群成员信息。"));
|
||||
} else {
|
||||
for (int i = 0; i < participants.length(); i++) {
|
||||
JSONObject participant = participants.optJSONObject(i);
|
||||
if (participant == null) continue;
|
||||
appendContent(buildMemberRow(participant));
|
||||
}
|
||||
}
|
||||
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildMemberRow(JSONObject participant) {
|
||||
boolean sourceProject = participant.optBoolean("isSourceProject", false);
|
||||
String participantProjectId = participant.optString("projectId", "");
|
||||
String title = participant.optString("threadDisplayName", "未命名线程");
|
||||
String subtitle = participant.optString("folderName", "");
|
||||
String meta = participant.optString("deviceId", "");
|
||||
String threadId = participant.optString("threadId", "");
|
||||
if (!threadId.isEmpty()) {
|
||||
meta = meta.isEmpty() ? threadId : meta + " · " + threadId;
|
||||
}
|
||||
return BossUi.buildListRow(
|
||||
this,
|
||||
title,
|
||||
subtitle,
|
||||
meta,
|
||||
sourceProject ? "当前" : null,
|
||||
v -> openProject(participantProjectId, title)
|
||||
);
|
||||
}
|
||||
|
||||
private void openProject(String targetProjectId, String targetProjectName) {
|
||||
if (targetProjectId == null || targetProjectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, ProjectDetailActivity.class);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, targetProjectId);
|
||||
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, targetProjectName);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void openRenameDialog() {
|
||||
final EditText input = BossUi.buildInput(this, "群名", false);
|
||||
input.setText(projectName == null ? "" : projectName);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("重命名群聊")
|
||||
.setView(input)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveGroupName(input.getText().toString().trim()))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void saveGroupName(String name) {
|
||||
if (name.isEmpty()) {
|
||||
showMessage("群名不能为空");
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.renameConversation(projectId, name, true);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
Intent result = new Intent();
|
||||
result.putExtra(EXTRA_PROJECT_NAME, name);
|
||||
setResult(RESULT_OK, result);
|
||||
showMessage("群名已更新");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String buildSubtitle(String folderName, int count) {
|
||||
String memberLabel = count <= 0 ? "暂无成员" : count + " 个成员";
|
||||
if (folderName.isEmpty()) {
|
||||
return memberLabel;
|
||||
}
|
||||
return folderName + " · " + memberLabel;
|
||||
}
|
||||
|
||||
private String buildDetailBody(JSONObject project, @Nullable JSONObject threadMeta) {
|
||||
String threadId = threadMeta == null ? project.optString("id", "") : threadMeta.optString("threadId", "");
|
||||
String folderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("群聊线程:").append(threadId.isEmpty() ? project.optString("id", "-") : threadId);
|
||||
builder.append("\n群聊名称:").append(project.optString("name", "群聊"));
|
||||
builder.append("\n文件夹:").append(folderName.isEmpty() ? "未命名文件夹" : folderName);
|
||||
builder.append("\n协作模式:").append(project.optString("collaborationMode", "development"));
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String buildDetailMeta(String projectId, String folderName, int count) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (!projectId.isEmpty()) {
|
||||
builder.append("project ").append(projectId);
|
||||
}
|
||||
if (!folderName.isEmpty()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(folderName);
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(count <= 0 ? "暂无成员" : "成员 " + count);
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private View loginPanel;
|
||||
private View contentPanel;
|
||||
private TextView loginTitle;
|
||||
private TextView loginHint;
|
||||
private Button loginButton;
|
||||
private ProgressBar loginProgress;
|
||||
@@ -55,6 +56,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private @Nullable JSONObject otaData;
|
||||
private @Nullable JSONArray conversationsData;
|
||||
private @Nullable JSONArray devicesData;
|
||||
private @Nullable String boundDeviceId;
|
||||
private @Nullable String boundDeviceName;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -107,6 +110,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void bindViews() {
|
||||
loginPanel = findViewById(R.id.login_panel);
|
||||
contentPanel = findViewById(R.id.content_panel);
|
||||
loginTitle = findViewById(R.id.login_title);
|
||||
loginHint = findViewById(R.id.login_hint);
|
||||
loginButton = findViewById(R.id.login_button);
|
||||
loginProgress = findViewById(R.id.login_progress);
|
||||
@@ -124,12 +128,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
tabConversations.setText(rootTabs[0]);
|
||||
tabDevices.setText(rootTabs[1]);
|
||||
tabMe.setText(rootTabs[2]);
|
||||
loginTitle.setText(WechatSurfaceMapper.loginTitle());
|
||||
loginHint.setText(WechatSurfaceMapper.loginHintText());
|
||||
loginButton.setText(WechatSurfaceMapper.loginButtonLabel());
|
||||
}
|
||||
|
||||
private void bindActions() {
|
||||
loginButton.setOnClickListener(v -> performAutoLogin());
|
||||
backButton.setVisibility(View.GONE);
|
||||
refreshButton.setOnClickListener(v -> refreshCurrentTab());
|
||||
refreshButton.setOnClickListener(v -> handleTopAction());
|
||||
tabConversations.setOnClickListener(v -> setActiveTab("conversations", true));
|
||||
tabDevices.setOnClickListener(v -> setActiveTab("devices", true));
|
||||
tabMe.setOnClickListener(v -> setActiveTab("me", true));
|
||||
@@ -146,7 +153,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void bootstrapSession() {
|
||||
showLogin("原生 Android 客户端已启用。点击下方按钮直接进入系统。");
|
||||
showLogin(WechatSurfaceMapper.loginHintText());
|
||||
if (!apiClient.hasSessionHints()) {
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +176,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
} catch (Exception ignored) {
|
||||
// Fall back to login panel.
|
||||
}
|
||||
runOnUiThread(() -> setLoginLoading(false, "点击登录后会直接进入系统。"));
|
||||
runOnUiThread(() -> setLoginLoading(false, WechatSurfaceMapper.loginHintText()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,10 +206,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private void refreshAllData(@Nullable JSONObject initialSession) {
|
||||
startRefreshing(true);
|
||||
topSubtitle.setText("正在同步最新数据...");
|
||||
topSubtitle.setText("");
|
||||
topSubtitle.setVisibility(View.GONE);
|
||||
executor.execute(() -> {
|
||||
JSONObject session;
|
||||
try {
|
||||
JSONObject session = initialSession;
|
||||
session = initialSession;
|
||||
if (session == null) {
|
||||
BossApiClient.ApiResponse sessionResponse = apiClient.getSession();
|
||||
if (!sessionResponse.ok()) {
|
||||
@@ -213,29 +222,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
session = sessionResponse.json.optJSONObject("session");
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse conversations = apiClient.getConversations();
|
||||
BossApiClient.ApiResponse devices = apiClient.getDevices();
|
||||
BossApiClient.ApiResponse ota = apiClient.getOtaStatus();
|
||||
BossApiClient.ApiResponse settings = apiClient.getSettings();
|
||||
if (!conversations.ok() || !devices.ok() || !ota.ok() || !settings.ok()) {
|
||||
throw new IOException("API_REFRESH_FAILED");
|
||||
}
|
||||
|
||||
JSONObject finalSession = session;
|
||||
runOnUiThread(() -> {
|
||||
sessionData = finalSession;
|
||||
conversationsData = conversations.json.optJSONArray("conversations");
|
||||
devicesData = devices.json.optJSONArray("devices");
|
||||
otaData = ota.json;
|
||||
JSONObject settingsPayload = settings.json.optJSONObject("settings");
|
||||
if (settingsPayload != null) {
|
||||
preferredEntryTab = settingsPayload.optString("preferredEntryPoint", "conversations");
|
||||
}
|
||||
maybeApplyPreferredEntry();
|
||||
renderCurrentTab();
|
||||
startRefreshing(false);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
startRefreshing(false);
|
||||
@@ -245,6 +231,94 @@ public class MainActivity extends AppCompatActivity {
|
||||
otaData = null;
|
||||
showLogin("当前登录已失效或同步失败,请重新点击登录。");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BossApiClient.ApiResponse conversations = null;
|
||||
BossApiClient.ApiResponse devices = null;
|
||||
BossApiClient.ApiResponse ota = null;
|
||||
BossApiClient.ApiResponse settings = null;
|
||||
boolean conversationsOk = false;
|
||||
boolean devicesOk = false;
|
||||
boolean otaOk = false;
|
||||
boolean settingsOk = false;
|
||||
|
||||
try {
|
||||
conversations = apiClient.getConversations();
|
||||
conversationsOk = conversations.ok();
|
||||
} catch (Exception ignored) {
|
||||
conversationsOk = false;
|
||||
}
|
||||
try {
|
||||
devices = apiClient.getDevices();
|
||||
devicesOk = devices.ok();
|
||||
} catch (Exception ignored) {
|
||||
devicesOk = false;
|
||||
}
|
||||
try {
|
||||
ota = apiClient.getOtaStatus();
|
||||
otaOk = ota.ok();
|
||||
} catch (Exception ignored) {
|
||||
otaOk = false;
|
||||
}
|
||||
try {
|
||||
settings = apiClient.getSettings();
|
||||
settingsOk = settings.ok();
|
||||
} catch (Exception ignored) {
|
||||
settingsOk = false;
|
||||
}
|
||||
|
||||
JSONObject finalSession = session;
|
||||
BossApiClient.ApiResponse finalConversations = conversations;
|
||||
BossApiClient.ApiResponse finalDevices = devices;
|
||||
BossApiClient.ApiResponse finalOta = ota;
|
||||
BossApiClient.ApiResponse finalSettings = settings;
|
||||
final boolean finalConversationsOk = conversationsOk;
|
||||
final boolean finalDevicesOk = devicesOk;
|
||||
final boolean finalOtaOk = otaOk;
|
||||
final boolean finalSettingsOk = settingsOk;
|
||||
runOnUiThread(() -> {
|
||||
sessionData = finalSession;
|
||||
conversationsData = WechatSurfaceMapper.resolveRefreshValue(
|
||||
conversationsData,
|
||||
finalConversations == null ? null : finalConversations.json.optJSONArray("conversations"),
|
||||
finalConversationsOk
|
||||
);
|
||||
devicesData = WechatSurfaceMapper.resolveRefreshValue(
|
||||
devicesData,
|
||||
finalDevices == null ? null : finalDevices.json.optJSONArray("devices"),
|
||||
finalDevicesOk
|
||||
);
|
||||
otaData = WechatSurfaceMapper.resolveRefreshValue(
|
||||
otaData,
|
||||
finalOta == null ? null : finalOta.json,
|
||||
finalOtaOk
|
||||
);
|
||||
JSONObject settingsPayload = finalSettings == null ? null : finalSettings.json.optJSONObject("settings");
|
||||
JSONObject userPayload = finalSettings == null ? null : finalSettings.json.optJSONObject("user");
|
||||
if (finalSettingsOk && settingsPayload != null) {
|
||||
preferredEntryTab = settingsPayload.optString("preferredEntryPoint", "conversations");
|
||||
}
|
||||
if (finalSettingsOk) {
|
||||
updateBoundDeviceState(userPayload);
|
||||
}
|
||||
maybeApplyPreferredEntry();
|
||||
renderCurrentTab();
|
||||
startRefreshing(false);
|
||||
if (!finalConversationsOk || !finalDevicesOk || !finalOtaOk || !finalSettingsOk) {
|
||||
showMessage("刷新失败,请稍后重试");
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
JSONObject finalSession = session;
|
||||
runOnUiThread(() -> {
|
||||
sessionData = finalSession == null ? sessionData : finalSession;
|
||||
maybeApplyPreferredEntry();
|
||||
renderCurrentTab();
|
||||
startRefreshing(false);
|
||||
showMessage("刷新失败,请稍后重试");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -264,7 +338,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void setLoginLoading(boolean loading, String hint) {
|
||||
loginProgress.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
loginButton.setEnabled(!loading);
|
||||
loginButton.setText(loading ? "处理中..." : "登录");
|
||||
loginButton.setText(loading ? "处理中..." : WechatSurfaceMapper.loginButtonLabel());
|
||||
loginHint.setText(hint);
|
||||
}
|
||||
|
||||
@@ -293,16 +367,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
switch (activeTab) {
|
||||
case "devices":
|
||||
updateHeader("设备", "生产设备列表");
|
||||
updateHeader("设备", "这里管理已接入设备与账号状态。");
|
||||
configureTopAction("+添加", true);
|
||||
renderDevicesRoot();
|
||||
break;
|
||||
case "me":
|
||||
updateHeader("我的", "账号与应用设置");
|
||||
updateHeader("我的", "");
|
||||
configureTopAction("刷新", false);
|
||||
renderMeRoot();
|
||||
break;
|
||||
case "conversations":
|
||||
default:
|
||||
updateHeader("会话", "最近消息");
|
||||
updateHeader("会话", WechatSurfaceMapper.conversationsHeaderSubtitle());
|
||||
configureTopAction("刷新", false);
|
||||
renderConversationsRoot();
|
||||
break;
|
||||
}
|
||||
@@ -311,6 +388,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void updateHeader(String title, String subtitle) {
|
||||
topTitle.setText(title);
|
||||
topSubtitle.setText(subtitle);
|
||||
topSubtitle.setVisibility(subtitle == null || subtitle.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private void updateTabStyles() {
|
||||
@@ -324,8 +402,33 @@ public class MainActivity extends AppCompatActivity {
|
||||
button.setTextColor(getColor(active ? R.color.boss_green : R.color.boss_text_muted));
|
||||
}
|
||||
|
||||
private void configureTopAction(String label, boolean primaryStyle) {
|
||||
refreshButton.setText(label);
|
||||
refreshButton.setBackgroundResource(primaryStyle ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
|
||||
refreshButton.setTextColor(getColor(primaryStyle ? R.color.boss_surface : R.color.boss_green));
|
||||
}
|
||||
|
||||
private void syncTopActionVisualState(boolean refreshing) {
|
||||
if ("devices".equals(activeTab)) {
|
||||
configureTopAction("+添加", true);
|
||||
refreshButton.setEnabled(true);
|
||||
return;
|
||||
}
|
||||
configureTopAction(refreshing ? "同步中" : "刷新", false);
|
||||
refreshButton.setEnabled(!refreshing);
|
||||
}
|
||||
|
||||
private void handleTopAction() {
|
||||
if ("devices".equals(activeTab)) {
|
||||
startActivity(new Intent(this, DeviceEnrollmentActivity.class));
|
||||
return;
|
||||
}
|
||||
refreshCurrentTab();
|
||||
}
|
||||
|
||||
private void renderConversationsRoot() {
|
||||
screenContent.removeAllViews();
|
||||
screenContent.addView(BossUi.buildHintPill(this, WechatSurfaceMapper.conversationsHintPillText()));
|
||||
if (conversationsData == null || conversationsData.length() == 0) {
|
||||
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有会话数据。"));
|
||||
return;
|
||||
@@ -336,33 +439,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (item == null) continue;
|
||||
String projectId = item.optString("projectId", "");
|
||||
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
|
||||
String badge = row.unreadCount > 0 ? String.valueOf(row.unreadCount) : null;
|
||||
screenContent.addView(BossUi.buildListRow(
|
||||
screenContent.addView(BossUi.buildConversationRow(
|
||||
this,
|
||||
row.title.isEmpty() ? "未命名会话" : row.title,
|
||||
row.preview.isEmpty() ? "暂无预览" : row.preview,
|
||||
row.timeLabel.isEmpty() ? null : row.timeLabel,
|
||||
badge,
|
||||
row,
|
||||
v -> {
|
||||
if (projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
openProject(projectId, row.title.isEmpty() ? "未命名会话" : row.title);
|
||||
String projectName = row.threadTitle.isEmpty() ? "未命名会话" : row.threadTitle;
|
||||
openProject(projectId, projectName);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void renderDevicesRoot() {
|
||||
screenContent.removeAllViews();
|
||||
screenContent.addView(BossUi.buildMenuRow(
|
||||
this,
|
||||
"添加设备",
|
||||
"通过配对码接入新的生产设备",
|
||||
null,
|
||||
v -> startActivity(new Intent(this, DeviceEnrollmentActivity.class))
|
||||
));
|
||||
|
||||
if (devicesData == null || devicesData.length() == 0) {
|
||||
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有接入设备。"));
|
||||
return;
|
||||
@@ -373,13 +465,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (item == null) continue;
|
||||
String deviceId = item.optString("id", "");
|
||||
WechatSurfaceMapper.DeviceRow row = WechatSurfaceMapper.toDeviceRow(item);
|
||||
screenContent.addView(BossUi.buildListRow(this, row.title, row.subtitle, null, null, v -> {
|
||||
if (deviceId.isEmpty()) {
|
||||
showMessage("缺少 deviceId");
|
||||
return;
|
||||
}
|
||||
openDevice(deviceId, row.title);
|
||||
}));
|
||||
screenContent.addView(BossUi.buildDeviceCard(
|
||||
this,
|
||||
row,
|
||||
v -> {
|
||||
if (deviceId.isEmpty()) {
|
||||
showMessage("缺少 deviceId");
|
||||
return;
|
||||
}
|
||||
openDevice(deviceId, row.title);
|
||||
},
|
||||
null
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,28 +488,23 @@ public class MainActivity extends AppCompatActivity {
|
||||
String account = sessionData == null
|
||||
? apiClient.getAccountLabel()
|
||||
: sessionData.optString("account", apiClient.getAccountLabel());
|
||||
screenContent.addView(BossUi.buildListRow(
|
||||
screenContent.addView(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
displayName,
|
||||
"账号 " + account,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
"ChatGPT Plus · 主账号",
|
||||
"主控账号已启用安全保护 · " + account
|
||||
));
|
||||
|
||||
for (String title : WechatSurfaceMapper.rootMeMenuTitles()) {
|
||||
screenContent.addView(BossUi.buildMenuRow(
|
||||
for (WechatSurfaceMapper.MeMenuItem item : WechatSurfaceMapper.rootMeMenuItems()) {
|
||||
screenContent.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
title,
|
||||
meDescriptionFor(title),
|
||||
meBadgeFor(title),
|
||||
v -> openMeEntry(title)
|
||||
item.title,
|
||||
item.description,
|
||||
null,
|
||||
meBadgeFor(item.key),
|
||||
v -> openMeEntry(item.key)
|
||||
));
|
||||
}
|
||||
|
||||
Button logoutButton = BossUi.buildSecondaryButton(this, "退出登录");
|
||||
logoutButton.setOnClickListener(v -> logout());
|
||||
screenContent.addView(logoutButton);
|
||||
}
|
||||
|
||||
private void logout() {
|
||||
@@ -450,58 +542,126 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private void startRefreshing(boolean refreshing) {
|
||||
screenRefresh.setRefreshing(refreshing);
|
||||
refreshButton.setEnabled(!refreshing);
|
||||
refreshButton.setText(refreshing ? "同步中" : "刷新");
|
||||
syncTopActionVisualState(refreshing);
|
||||
}
|
||||
|
||||
private void showMessage(String text) {
|
||||
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void openMeEntry(String title) {
|
||||
private void openMeEntry(String key) {
|
||||
Intent intent;
|
||||
switch (title) {
|
||||
case "账号与安全":
|
||||
switch (key) {
|
||||
case "security":
|
||||
intent = new Intent(this, SecurityActivity.class);
|
||||
break;
|
||||
case "AI 账号":
|
||||
case "ai_accounts":
|
||||
intent = new Intent(this, AiAccountsActivity.class);
|
||||
break;
|
||||
case "设置":
|
||||
case "settings":
|
||||
intent = new Intent(this, SettingsActivity.class);
|
||||
break;
|
||||
case "技能":
|
||||
intent = new Intent(this, SkillInventoryActivity.class);
|
||||
case "ops":
|
||||
intent = new Intent(this, OpsCenterActivity.class);
|
||||
break;
|
||||
case "关于":
|
||||
case "skills":
|
||||
openSkillInventoryFromMe();
|
||||
return;
|
||||
case "about":
|
||||
intent = new Intent(this, AboutActivity.class);
|
||||
break;
|
||||
default:
|
||||
showMessage("暂未接入:" + title);
|
||||
showMessage("暂未接入:" + key);
|
||||
return;
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private String meDescriptionFor(String title) {
|
||||
switch (title) {
|
||||
case "账号与安全":
|
||||
return "查看当前会话与登录安全";
|
||||
case "AI 账号":
|
||||
return "管理主 GPT、备用 GPT 与 API 容灾";
|
||||
case "设置":
|
||||
return "调整默认首页和提醒偏好";
|
||||
case "技能":
|
||||
return "按设备查看 Skill 清单";
|
||||
case "关于":
|
||||
return "查看版本、更新与高级入口";
|
||||
default:
|
||||
return "";
|
||||
private void openSkillInventoryFromMe() {
|
||||
String targetDeviceId = resolveSkillTargetDeviceId();
|
||||
if (targetDeviceId == null || targetDeviceId.isEmpty()) {
|
||||
showMessage("当前没有可确定的绑定设备,请先到设备页确认。");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, SkillInventoryActivity.class);
|
||||
intent.putExtra(SkillInventoryActivity.EXTRA_DEVICE_ID, targetDeviceId);
|
||||
String targetDeviceName = resolveDeviceName(targetDeviceId);
|
||||
if (targetDeviceName != null && !targetDeviceName.isEmpty()) {
|
||||
intent.putExtra(SkillInventoryActivity.EXTRA_DEVICE_NAME, targetDeviceName);
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private @Nullable String meBadgeFor(String title) {
|
||||
if ("关于".equals(title) && otaData != null && otaData.optBoolean("hasOta", false)) {
|
||||
private void updateBoundDeviceState(@Nullable JSONObject userPayload) {
|
||||
String nextBoundDeviceId = userPayload == null ? "" : userPayload.optString("boundDeviceId", "");
|
||||
boundDeviceId = nextBoundDeviceId.isEmpty() ? null : nextBoundDeviceId;
|
||||
boundDeviceName = resolveDeviceName(boundDeviceId);
|
||||
}
|
||||
|
||||
private @Nullable String resolveSkillTargetDeviceId() {
|
||||
if (devicesData == null || devicesData.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (boundDeviceId != null && !boundDeviceId.isEmpty()) {
|
||||
String validatedBoundDeviceId = findDeviceId(devicesData, boundDeviceId);
|
||||
if (validatedBoundDeviceId != null) {
|
||||
return validatedBoundDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
String account = sessionData == null ? apiClient.getAccountLabel() : sessionData.optString("account", apiClient.getAccountLabel());
|
||||
String matchedByAccount = null;
|
||||
for (int i = 0; i < devicesData.length(); i++) {
|
||||
JSONObject device = devicesData.optJSONObject(i);
|
||||
if (device == null) continue;
|
||||
String deviceAccount = device.optString("account", "");
|
||||
if (!account.isEmpty() && account.equals(deviceAccount)) {
|
||||
matchedByAccount = device.optString("id", "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchedByAccount != null && !matchedByAccount.isEmpty()) {
|
||||
return matchedByAccount;
|
||||
}
|
||||
if (devicesData.length() == 1) {
|
||||
JSONObject onlyDevice = devicesData.optJSONObject(0);
|
||||
return onlyDevice == null ? null : onlyDevice.optString("id", "");
|
||||
}
|
||||
JSONObject fallback = devicesData.optJSONObject(0);
|
||||
return fallback == null ? null : fallback.optString("id", "");
|
||||
}
|
||||
|
||||
private static @Nullable String findDeviceId(JSONArray devices, @Nullable String candidateDeviceId) {
|
||||
if (candidateDeviceId == null || candidateDeviceId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < devices.length(); i++) {
|
||||
JSONObject device = devices.optJSONObject(i);
|
||||
if (device == null) continue;
|
||||
if (candidateDeviceId.equals(device.optString("id", ""))) {
|
||||
return candidateDeviceId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable String resolveDeviceName(@Nullable String deviceId) {
|
||||
if (deviceId == null || deviceId.isEmpty() || devicesData == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < devicesData.length(); i++) {
|
||||
JSONObject device = devicesData.optJSONObject(i);
|
||||
if (device == null) continue;
|
||||
if (deviceId.equals(device.optString("id", ""))) {
|
||||
return device.optString("name", deviceId);
|
||||
}
|
||||
}
|
||||
return boundDeviceName;
|
||||
}
|
||||
|
||||
private @Nullable String meBadgeFor(String key) {
|
||||
if ("about".equals(key) && otaData != null && otaData.optBoolean("hasOta", false)) {
|
||||
return "OTA";
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -10,18 +10,12 @@ import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class OpsCenterActivity extends BossScreenActivity {
|
||||
private enum Tab {
|
||||
OPS,
|
||||
AUDIT
|
||||
}
|
||||
|
||||
private Tab activeTab = Tab.OPS;
|
||||
private LinearLayout contentRoot;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
configureScreen("运维中心", "运维对话 / 审计对话");
|
||||
configureScreen("运维与修复", "运维会话、修复回放与 standby 切换");
|
||||
setHeaderAction("刷新", v -> reload());
|
||||
contentRoot = new LinearLayout(this);
|
||||
contentRoot.setOrientation(LinearLayout.VERTICAL);
|
||||
@@ -35,62 +29,34 @@ public class OpsCenterActivity extends BossScreenActivity {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse ops = apiClient.getOpsSummary();
|
||||
BossApiClient.ApiResponse audit = apiClient.getAuditSummary();
|
||||
if (!ops.ok() || !audit.ok()) {
|
||||
throw new IllegalStateException("OPS_OR_AUDIT_LOAD_FAILED");
|
||||
if (!ops.ok()) {
|
||||
throw new IllegalStateException("OPS_LOAD_FAILED");
|
||||
}
|
||||
runOnUiThread(() -> render(ops.json, audit.json));
|
||||
runOnUiThread(() -> render(ops.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
replaceContent(BossUi.buildEmptyCard(this, "运维中心加载失败:" + error.getMessage()));
|
||||
replaceContent(BossUi.buildEmptyCard(this, "运维与修复加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void render(JSONObject ops, JSONObject audit) {
|
||||
private void render(JSONObject ops) {
|
||||
replaceContent(contentRoot);
|
||||
contentRoot.removeAllViews();
|
||||
contentRoot.addView(buildTabBar());
|
||||
if (activeTab == Tab.OPS) {
|
||||
renderOpsTab(ops);
|
||||
} else {
|
||||
renderAuditTab(audit);
|
||||
}
|
||||
renderOpsTab(ops);
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildTabBar() {
|
||||
LinearLayout bar = new LinearLayout(this);
|
||||
bar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
bar.addView(buildTabButton("运维对话", activeTab == Tab.OPS, v -> {
|
||||
activeTab = Tab.OPS;
|
||||
reload();
|
||||
}));
|
||||
bar.addView(buildTabButton("审计对话", activeTab == Tab.AUDIT, v -> {
|
||||
activeTab = Tab.AUDIT;
|
||||
reload();
|
||||
}));
|
||||
return bar;
|
||||
}
|
||||
|
||||
private Button buildTabButton(String label, boolean active, android.view.View.OnClickListener listener) {
|
||||
Button button = BossUi.buildPrimaryButton(this, label);
|
||||
button.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
|
||||
button.setBackgroundResource(active ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
|
||||
button.setTextColor(getColor(active ? R.color.boss_surface : R.color.boss_green));
|
||||
button.setOnClickListener(listener);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void renderOpsTab(JSONObject ops) {
|
||||
contentRoot.addView(BossUi.buildCard(
|
||||
contentRoot.addView(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"当前巡检模式",
|
||||
"巡检状态",
|
||||
ops.optString("mode", "idle").equals("active")
|
||||
? "active:当前存在风险线程或未关闭运维工单。"
|
||||
: "idle:当前没有高风险工单,保持低频巡检。",
|
||||
"来源:/api/v1/ops/summary"
|
||||
"这里只保留修复与验证的轻量入口。"
|
||||
));
|
||||
|
||||
JSONArray faults = ops.optJSONArray("faults");
|
||||
@@ -106,7 +72,9 @@ public class OpsCenterActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
private LinearLayout buildFaultCard(JSONObject fault, @Nullable JSONArray tickets) {
|
||||
LinearLayout card = BossUi.buildCard(
|
||||
LinearLayout card = new LinearLayout(this);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
fault.optString("faultKey", "故障"),
|
||||
fault.optString("summary", "暂无摘要"),
|
||||
@@ -114,13 +82,9 @@ public class OpsCenterActivity extends BossScreenActivity {
|
||||
+ " · " + fault.optString("status", "-")
|
||||
+ " · " + fault.optString("nodeId", "-")
|
||||
+ " · " + fault.optString("serviceName", "-")
|
||||
);
|
||||
|
||||
card.addView(BossUi.buildCard(
|
||||
this,
|
||||
"建议动作",
|
||||
fault.optString("suggestedNextAction", "暂无"),
|
||||
"trace " + fault.optString("traceId", "-")
|
||||
+ " · 建议 " + fault.optString("suggestedNextAction", "暂无"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
if (tickets != null) {
|
||||
@@ -135,122 +99,38 @@ public class OpsCenterActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
private LinearLayout buildTicketCard(JSONObject ticket) {
|
||||
LinearLayout card = BossUi.buildCard(
|
||||
LinearLayout card = new LinearLayout(this);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
ticket.optString("title", "修复工单"),
|
||||
ticket.optString("actionSummary", "暂无动作摘要"),
|
||||
ticket.optString("approvalStatus", "-")
|
||||
+ " · " + ticket.optString("executionStatus", "-")
|
||||
+ " · " + ticket.optString("targetNodeId", "-")
|
||||
);
|
||||
+ " · " + ticket.optString("updatedAt", "-"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
if (ticket.optJSONObject("verification") != null) {
|
||||
JSONObject verification = ticket.optJSONObject("verification");
|
||||
card.addView(BossUi.buildCard(
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"验证结果",
|
||||
verification.optString("summary", "暂无"),
|
||||
verification.optString("status", "-")
|
||||
+ " · " + verification.optString("verifiedAt", "-")
|
||||
+ " · " + verification.optString("verifiedAt", "-"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
Button approve = BossUi.buildPrimaryButton(this, "批准修复");
|
||||
Button approve = BossUi.buildMiniActionButton(this, "批准修复", true);
|
||||
approve.setOnClickListener(v -> approveTicket(ticket.optString("ticketId")));
|
||||
card.addView(approve);
|
||||
|
||||
Button verify = BossUi.buildSecondaryButton(this, "验证修复");
|
||||
Button verify = BossUi.buildMiniActionButton(this, "验证修复", false);
|
||||
verify.setOnClickListener(v -> verifyTicket(ticket.optString("ticketId")));
|
||||
card.addView(verify);
|
||||
return card;
|
||||
}
|
||||
|
||||
private void renderAuditTab(JSONObject audit) {
|
||||
contentRoot.addView(BossUi.buildCard(
|
||||
this,
|
||||
"审计概要",
|
||||
"待处理请求 " + (audit.optJSONArray("pendingRequests") == null ? 0 : audit.optJSONArray("pendingRequests").length())
|
||||
+ "\n最新结果 " + (audit.optJSONArray("latestResults") == null ? 0 : audit.optJSONArray("latestResults").length()),
|
||||
"来源:/api/v1/audits/summary"
|
||||
));
|
||||
|
||||
JSONArray pendingRequests = audit.optJSONArray("pendingRequests");
|
||||
if (pendingRequests == null || pendingRequests.length() == 0) {
|
||||
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待处理的审计请求。"));
|
||||
} else {
|
||||
for (int i = 0; i < pendingRequests.length(); i++) {
|
||||
JSONObject request = pendingRequests.optJSONObject(i);
|
||||
if (request == null) continue;
|
||||
contentRoot.addView(buildAuditRequestCard(request));
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray latestResults = audit.optJSONArray("latestResults");
|
||||
if (latestResults != null && latestResults.length() > 0) {
|
||||
contentRoot.addView(BossUi.buildCard(this, "审计结果", "最近完成的审计会展示在这里。", "可回看 decision / findings"));
|
||||
for (int i = 0; i < latestResults.length(); i++) {
|
||||
JSONObject result = latestResults.optJSONObject(i);
|
||||
if (result == null) continue;
|
||||
contentRoot.addView(buildAuditResultCard(result));
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray capabilities = audit.optJSONArray("capabilities");
|
||||
if (capabilities != null && capabilities.length() > 0) {
|
||||
contentRoot.addView(BossUi.buildCard(this, "能力注册表", "展示当前设备上的可用能力。", "与审计请求的 capabilityRequirements 对应"));
|
||||
for (int i = 0; i < capabilities.length(); i++) {
|
||||
JSONObject capability = capabilities.optJSONObject(i);
|
||||
if (capability == null) continue;
|
||||
contentRoot.addView(BossUi.buildCard(
|
||||
this,
|
||||
capability.optString("displayName", "能力"),
|
||||
capability.optString("capabilityType", "-")
|
||||
+ "\n提供者:" + capability.optString("providerId", "-")
|
||||
+ "\n模式:" + capability.optString("leaseMode", "-")
|
||||
+ "\n动作:" + joinArray(capability.optJSONArray("supportedActions")),
|
||||
capability.optString("status", "-")
|
||||
+ " · " + capability.optString("healthStatus", "-")
|
||||
+ " · " + capability.optString("nodeId", "-")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LinearLayout buildAuditRequestCard(JSONObject request) {
|
||||
LinearLayout card = BossUi.buildCard(
|
||||
this,
|
||||
request.optString("projectName", "审计请求"),
|
||||
request.optString("objective", "暂无目标"),
|
||||
request.optString("auditType", "-")
|
||||
+ " · priority " + request.optInt("priority", 0)
|
||||
+ " · " + request.optString("trigger", "-")
|
||||
);
|
||||
card.addView(BossUi.buildCard(
|
||||
this,
|
||||
"审计条件",
|
||||
"要求:" + joinStringArray(request.optJSONArray("acceptanceCriteria"))
|
||||
+ "\n风险:" + joinStringArray(request.optJSONArray("riskFocus"))
|
||||
+ "\n证据:" + joinStringArray(request.optJSONArray("evidenceRefs")),
|
||||
"时限 " + request.optInt("timeBudgetSeconds", 0) + " 秒"
|
||||
));
|
||||
return card;
|
||||
}
|
||||
|
||||
private LinearLayout buildAuditResultCard(JSONObject result) {
|
||||
LinearLayout card = BossUi.buildCard(
|
||||
this,
|
||||
result.optString("decision", "result"),
|
||||
result.optString("summary", "暂无摘要"),
|
||||
result.optString("status", "-")
|
||||
+ " · confidence " + result.optDouble("confidence", 0.0)
|
||||
+ " · " + result.optString("completedAt", "-")
|
||||
);
|
||||
card.addView(BossUi.buildCard(
|
||||
this,
|
||||
"审计发现",
|
||||
joinStringArray(result.optJSONArray("findings")),
|
||||
"需要动作:" + joinStringArray(result.optJSONArray("requiredActions"))
|
||||
));
|
||||
card.addView(BossUi.buildInlineActionRow(this, approve, verify));
|
||||
return card;
|
||||
}
|
||||
|
||||
@@ -311,16 +191,4 @@ public class OpsCenterActivity extends BossScreenActivity {
|
||||
}
|
||||
return builder.length() == 0 ? "-" : builder.toString();
|
||||
}
|
||||
|
||||
private String joinStringArray(@Nullable JSONArray values) {
|
||||
if (values == null || values.length() == 0) return "-";
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < values.length(); i++) {
|
||||
String value = values.optString(i);
|
||||
if (value == null || value.isEmpty()) continue;
|
||||
if (builder.length() > 0) builder.append(";");
|
||||
builder.append(value);
|
||||
}
|
||||
return builder.length() == 0 ? "-" : builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -22,6 +24,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
|
||||
private String projectId;
|
||||
private String initialProjectName;
|
||||
private boolean projectIsGroup;
|
||||
private String projectFolderName;
|
||||
private LinearLayout quickActionsLayout;
|
||||
private EditText composerInput;
|
||||
private Button composerSendButton;
|
||||
@@ -30,6 +34,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private boolean composerSending;
|
||||
private boolean renderNearBottom;
|
||||
private boolean renderForcedScrollToBottom;
|
||||
private ActivityResultLauncher<Intent> conversationInfoLauncher;
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
@@ -45,9 +50,25 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
composerInput = findViewById(R.id.project_chat_input);
|
||||
composerSendButton = findViewById(R.id.project_chat_send);
|
||||
chatScrollView = findViewById(R.id.project_chat_scroll);
|
||||
conversationInfoLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() != RESULT_OK) {
|
||||
return;
|
||||
}
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
String updatedTitle = data.getStringExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME);
|
||||
if (!TextUtils.isEmpty(updatedTitle)) {
|
||||
initialProjectName = updatedTitle;
|
||||
configureScreen(updatedTitle, "正在同步项目详情...");
|
||||
}
|
||||
}
|
||||
reload();
|
||||
}
|
||||
);
|
||||
|
||||
configureScreen(initialProjectName == null ? "项目详情" : initialProjectName, "正在同步项目详情...");
|
||||
hideHeaderAction();
|
||||
composerSendButton.setOnClickListener(v -> sendTextMessageFromComposer());
|
||||
composerInput.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
@@ -113,10 +134,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private void renderProject(JSONObject payload) {
|
||||
JSONObject project = payload.optJSONObject("project");
|
||||
JSONArray devices = payload.optJSONArray("devices");
|
||||
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
|
||||
|
||||
String title = project != null ? project.optString("name", "项目详情") : "项目详情";
|
||||
initialProjectName = title;
|
||||
configureScreen(title, "设备:" + joinDeviceNames(devices));
|
||||
projectIsGroup = project != null && project.optBoolean("isGroup", false);
|
||||
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
configureScreen(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
setHeaderAction(WechatSurfaceMapper.conversationInfoActionLabel(), v -> openConversationInfo());
|
||||
|
||||
renderQuickActions();
|
||||
replaceContent();
|
||||
@@ -242,6 +267,17 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void openConversationInfo() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, WechatSurfaceMapper.resolveConversationInfoTargetClass(projectIsGroup));
|
||||
intent.putExtra(ConversationInfoActivity.EXTRA_PROJECT_ID, projectId);
|
||||
intent.putExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME, initialProjectName);
|
||||
conversationInfoLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private String joinDeviceNames(@Nullable JSONArray devices) {
|
||||
if (devices == null || devices.length() == 0) {
|
||||
return "未绑定设备";
|
||||
@@ -260,6 +296,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return builder.length() == 0 ? "未绑定设备" : builder.toString();
|
||||
}
|
||||
|
||||
private String buildProjectSubtitle(String folderName, @Nullable JSONArray devices) {
|
||||
String deviceLabel = joinDeviceNames(devices);
|
||||
if (TextUtils.isEmpty(folderName)) {
|
||||
return "设备:" + deviceLabel;
|
||||
}
|
||||
return folderName + " · 设备:" + deviceLabel;
|
||||
}
|
||||
|
||||
private void scrollChatToBottom() {
|
||||
if (chatScrollView == null) {
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,7 @@ public class SecurityActivity extends BossScreenActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
configureScreen("账号与安全", "原生会话与设备安全");
|
||||
configureScreen("账号与安全", "登录会话与设备保护");
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -34,16 +34,14 @@ public class SecurityActivity extends BossScreenActivity {
|
||||
|
||||
private void renderSecurity(@Nullable JSONObject session) {
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildListRow(
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"当前登录模式",
|
||||
"当前客户端仍使用快速进入模式。",
|
||||
"需要更严格认证时,再切回账号密码或验证码登录。",
|
||||
null,
|
||||
null
|
||||
"需要更严格认证时,再切回账号密码或验证码登录。"
|
||||
));
|
||||
if (session != null) {
|
||||
appendContent(BossUi.buildListRow(
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"当前会话",
|
||||
"账号 " + session.optString("account", "-")
|
||||
|
||||
@@ -15,13 +15,15 @@ public class SettingsActivity extends BossScreenActivity {
|
||||
private SwitchCompat riskBadgesSwitch;
|
||||
private SwitchCompat confirmActionsSwitch;
|
||||
private Spinner preferredEntrySpinner;
|
||||
private boolean settingsLoaded = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
configureScreen("设置", "原生偏好配置");
|
||||
configureScreen("设置", "默认首页与提醒偏好");
|
||||
setHeaderAction("保存", v -> saveSettings());
|
||||
buildForm();
|
||||
buildFormContent();
|
||||
updateSaveAvailability();
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -36,48 +38,52 @@ public class SettingsActivity extends BossScreenActivity {
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
settingsLoaded = false;
|
||||
updateSaveAvailability();
|
||||
replaceContent(BossUi.buildEmptyCard(this, "设置加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildForm() {
|
||||
replaceContent(BossUi.buildListRow(
|
||||
private void buildFormContent() {
|
||||
if (liveUpdatesSwitch == null) {
|
||||
liveUpdatesSwitch = new SwitchCompat(this);
|
||||
liveUpdatesSwitch.setText("启用实时刷新");
|
||||
}
|
||||
if (riskBadgesSwitch == null) {
|
||||
riskBadgesSwitch = new SwitchCompat(this);
|
||||
riskBadgesSwitch.setText("显示风险徽标");
|
||||
}
|
||||
if (confirmActionsSwitch == null) {
|
||||
confirmActionsSwitch = new SwitchCompat(this);
|
||||
confirmActionsSwitch.setText("危险操作前确认");
|
||||
}
|
||||
if (preferredEntrySpinner == null) {
|
||||
preferredEntrySpinner = new Spinner(this);
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(
|
||||
this,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
new String[]{"conversations", "devices", "me"}
|
||||
);
|
||||
preferredEntrySpinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
replaceContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"偏好设置",
|
||||
"调整默认首页和提醒行为。",
|
||||
"保存后会直接写入 /api/v1/settings。",
|
||||
null,
|
||||
null
|
||||
"保存后会直接写入 /api/v1/settings。"
|
||||
));
|
||||
|
||||
liveUpdatesSwitch = new SwitchCompat(this);
|
||||
liveUpdatesSwitch.setText("启用实时刷新");
|
||||
|
||||
riskBadgesSwitch = new SwitchCompat(this);
|
||||
riskBadgesSwitch.setText("显示风险徽标");
|
||||
|
||||
confirmActionsSwitch = new SwitchCompat(this);
|
||||
confirmActionsSwitch.setText("危险操作前确认");
|
||||
|
||||
preferredEntrySpinner = new Spinner(this);
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(
|
||||
this,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
new String[]{"conversations", "devices", "me"}
|
||||
);
|
||||
preferredEntrySpinner.setAdapter(adapter);
|
||||
|
||||
LinearLayout form = BossUi.buildCard(this, "交互偏好", "切换默认首页与提醒开关。", "保存后立即生效");
|
||||
form.addView(liveUpdatesSwitch);
|
||||
form.addView(riskBadgesSwitch);
|
||||
form.addView(confirmActionsSwitch);
|
||||
form.addView(preferredEntrySpinner);
|
||||
appendContent(form);
|
||||
appendContent(BossUi.buildFormCell(this, "实时刷新", "会话、设备和 OTA 状态变化时自动更新", liveUpdatesSwitch));
|
||||
appendContent(BossUi.buildFormCell(this, "风险徽标", "在列表中显示风险状态提示", riskBadgesSwitch));
|
||||
appendContent(BossUi.buildFormCell(this, "危险操作确认", "执行修复或切换前再次确认", confirmActionsSwitch));
|
||||
appendContent(BossUi.buildFormCell(this, "默认首页", "下次打开 App 优先进入这里", preferredEntrySpinner));
|
||||
}
|
||||
|
||||
private void populate(@Nullable JSONObject settings) {
|
||||
buildFormContent();
|
||||
if (settings != null) {
|
||||
liveUpdatesSwitch.setChecked(settings.optBoolean("liveUpdates", true));
|
||||
riskBadgesSwitch.setChecked(settings.optBoolean("showRiskBadges", true));
|
||||
@@ -91,10 +97,16 @@ public class SettingsActivity extends BossScreenActivity {
|
||||
preferredEntrySpinner.setSelection(0);
|
||||
}
|
||||
}
|
||||
settingsLoaded = settings != null;
|
||||
updateSaveAvailability();
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private void saveSettings() {
|
||||
if (!settingsLoaded) {
|
||||
showMessage("设置尚未加载完成,请先刷新成功后再保存。");
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -117,4 +129,11 @@ public class SettingsActivity extends BossScreenActivity {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSaveAvailability() {
|
||||
if (headerActionButton != null) {
|
||||
headerActionButton.setEnabled(settingsLoaded);
|
||||
headerActionButton.setAlpha(settingsLoaded ? 1f : 0.45f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,15 @@ public class SkillInventoryActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
private String resolveTargetDeviceId() throws Exception {
|
||||
if (deviceId != null && !deviceId.isEmpty()) {
|
||||
return deviceId;
|
||||
String explicitDeviceId = deviceId;
|
||||
String boundDeviceId = null;
|
||||
BossApiClient.ApiResponse settingsResponse = apiClient.getSettings();
|
||||
if (settingsResponse.ok()) {
|
||||
JSONObject user = settingsResponse.json.optJSONObject("user");
|
||||
if (user != null) {
|
||||
String candidate = user.optString("boundDeviceId", "");
|
||||
boundDeviceId = candidate.isEmpty() ? null : candidate;
|
||||
}
|
||||
}
|
||||
BossApiClient.ApiResponse response = apiClient.getDevices();
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
@@ -54,7 +61,54 @@ public class SkillInventoryActivity extends BossScreenActivity {
|
||||
if (devices == null || devices.length() == 0) {
|
||||
throw new IllegalStateException("NO_DEVICE");
|
||||
}
|
||||
return devices.optJSONObject(0).optString("id");
|
||||
return chooseTargetDeviceId(explicitDeviceId, boundDeviceId, apiClient.getAccountLabel(), devices);
|
||||
}
|
||||
|
||||
private static String chooseTargetDeviceId(
|
||||
@Nullable String explicitDeviceId,
|
||||
@Nullable String boundDeviceId,
|
||||
String account,
|
||||
JSONArray devices
|
||||
) {
|
||||
String explicitMatch = findDeviceId(devices, explicitDeviceId);
|
||||
if (explicitMatch != null) {
|
||||
return explicitMatch;
|
||||
}
|
||||
|
||||
String boundMatch = findDeviceId(devices, boundDeviceId);
|
||||
if (boundMatch != null) {
|
||||
return boundMatch;
|
||||
}
|
||||
|
||||
for (int i = 0; i < devices.length(); i++) {
|
||||
JSONObject device = devices.optJSONObject(i);
|
||||
if (device == null) continue;
|
||||
if (account.equals(device.optString("account", ""))) {
|
||||
return device.optString("id", "");
|
||||
}
|
||||
}
|
||||
if (devices.length() == 1) {
|
||||
JSONObject onlyDevice = devices.optJSONObject(0);
|
||||
if (onlyDevice != null) {
|
||||
return onlyDevice.optString("id", "");
|
||||
}
|
||||
}
|
||||
JSONObject fallback = devices.optJSONObject(0);
|
||||
return fallback == null ? "" : fallback.optString("id", "");
|
||||
}
|
||||
|
||||
private static @Nullable String findDeviceId(JSONArray devices, @Nullable String candidateDeviceId) {
|
||||
if (candidateDeviceId == null || candidateDeviceId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < devices.length(); i++) {
|
||||
JSONObject device = devices.optJSONObject(i);
|
||||
if (device == null) continue;
|
||||
if (candidateDeviceId.equals(device.optString("id", ""))) {
|
||||
return candidateDeviceId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void renderSkills(JSONObject payload) {
|
||||
@@ -65,13 +119,11 @@ public class SkillInventoryActivity extends BossScreenActivity {
|
||||
if (device != null) {
|
||||
deviceName = device.optString("name", deviceId);
|
||||
configureScreen("技能", deviceName);
|
||||
appendContent(BossUi.buildListRow(
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
deviceName,
|
||||
"当前页按设备查看 Skill 清单。",
|
||||
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。",
|
||||
null,
|
||||
null
|
||||
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -85,7 +137,7 @@ public class SkillInventoryActivity extends BossScreenActivity {
|
||||
if (skill == null) continue;
|
||||
LinearLayout card = new LinearLayout(this);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
card.addView(BossUi.buildListRow(
|
||||
card.addView(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
skill.optString("name", "未命名 Skill"),
|
||||
skill.optString("description", "未提供说明"),
|
||||
@@ -94,12 +146,11 @@ public class SkillInventoryActivity extends BossScreenActivity {
|
||||
null,
|
||||
null
|
||||
));
|
||||
Button copyInvocation = BossUi.buildPrimaryButton(this, "复制调用语句");
|
||||
Button copyInvocation = BossUi.buildMiniActionButton(this, "复制调用", true);
|
||||
copyInvocation.setOnClickListener(v -> BossUi.copyText(this, "Skill 调用", skill.optString("invocation", "")));
|
||||
card.addView(copyInvocation);
|
||||
Button copyPath = BossUi.buildSecondaryButton(this, "复制路径");
|
||||
Button copyPath = BossUi.buildMiniActionButton(this, "复制路径", false);
|
||||
copyPath.setOnClickListener(v -> BossUi.copyText(this, "Skill 路径", skill.optString("path", "")));
|
||||
card.addView(copyPath);
|
||||
card.addView(BossUi.buildInlineActionRow(this, copyInvocation, copyPath));
|
||||
appendContent(card);
|
||||
}
|
||||
setRefreshing(false);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.hyzq.boss;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -13,12 +14,13 @@ public final class WechatSurfaceMapper {
|
||||
"我的"
|
||||
);
|
||||
|
||||
private static final List<String> ROOT_ME_MENU_TITLES = Arrays.asList(
|
||||
"账号与安全",
|
||||
"AI 账号",
|
||||
"设置",
|
||||
"技能",
|
||||
"关于"
|
||||
private static final List<MeMenuItem> ROOT_ME_MENU_ITEMS = Arrays.asList(
|
||||
new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"),
|
||||
new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"),
|
||||
new MeMenuItem("ops", "运维与修复", "查看运维会话、修复回放与 standby 切换"),
|
||||
new MeMenuItem("ai_accounts", "AI 账号", "管理主 GPT、备用 GPT 与 API 容灾"),
|
||||
new MeMenuItem("skills", "技能", "按设备查看 Skill 清单"),
|
||||
new MeMenuItem("about", "关于", "当前版本、OTA 状态与更新内容")
|
||||
);
|
||||
|
||||
private static final List<String> PROJECT_QUICK_ACTIONS = Arrays.asList(
|
||||
@@ -37,11 +39,33 @@ public final class WechatSurfaceMapper {
|
||||
|
||||
public static ConversationRow toConversationRow(JSONObject item) {
|
||||
JSONObject source = item == null ? new JSONObject() : item;
|
||||
JSONArray members = source.optJSONArray("groupMembers");
|
||||
List<GroupAvatarMember> groupAvatarMembers = new ArrayList<>();
|
||||
if (members != null) {
|
||||
for (int i = 0; i < members.length(); i++) {
|
||||
JSONObject member = members.optJSONObject(i);
|
||||
if (member == null) continue;
|
||||
groupAvatarMembers.add(new GroupAvatarMember(
|
||||
member.optString("threadId", ""),
|
||||
member.optString("avatar", ""),
|
||||
member.optString("title", "")
|
||||
));
|
||||
}
|
||||
}
|
||||
JSONObject avatar = source.optJSONObject("avatar");
|
||||
boolean isGroup = source.optBoolean("isGroup", groupAvatarMembers.size() > 1);
|
||||
return new ConversationRow(
|
||||
source.optString("title", source.optString("projectTitle", "")),
|
||||
source.optString("preview", ""),
|
||||
source.optString("threadTitle", source.optString("title", source.optString("projectTitle", ""))),
|
||||
source.optString("folderLabel", ""),
|
||||
source.optString("lastMessagePreview", source.optString("preview", "")),
|
||||
source.optString("timeLabel", source.optString("latestReplyLabel", "")),
|
||||
source.optInt("unreadCount", 0)
|
||||
source.optInt("unreadCount", 0),
|
||||
source.optString("topPinnedLabel", ""),
|
||||
source.optInt("activityIconCount", 0),
|
||||
isGroup,
|
||||
isGroup ? "" : avatar == null ? "" : avatar.optString("primary", ""),
|
||||
isGroup ? "" : avatar == null ? "" : avatar.optString("secondary", ""),
|
||||
groupAvatarMembers.toArray(new GroupAvatarMember[0])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,16 +73,20 @@ public final class WechatSurfaceMapper {
|
||||
JSONObject source = item == null ? new JSONObject() : item;
|
||||
return new DeviceRow(
|
||||
source.optString("title", source.optString("name", "")),
|
||||
buildSubtitle(source)
|
||||
buildDeviceAccountProjectLine(source),
|
||||
buildDeviceQuotaStatusLine(source),
|
||||
source.optString("avatar", ""),
|
||||
resolveDeviceStatusKey(source)
|
||||
);
|
||||
}
|
||||
|
||||
public static DeviceDetailSummary toDeviceDetailSummary(JSONObject item) {
|
||||
JSONObject source = item == null ? new JSONObject() : item;
|
||||
DeviceRow row = toDeviceRow(source);
|
||||
return new DeviceDetailSummary(
|
||||
source.optString("title", source.optString("name", "")),
|
||||
buildSubtitle(source),
|
||||
buildDetailMeta(source)
|
||||
row.title,
|
||||
row.subtitle,
|
||||
mergeDeviceMeta(row.meta, buildDetailMeta(source))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,31 +95,83 @@ public final class WechatSurfaceMapper {
|
||||
}
|
||||
|
||||
public static String[] rootMeMenuTitles() {
|
||||
return ROOT_ME_MENU_TITLES.toArray(new String[0]);
|
||||
String[] titles = new String[ROOT_ME_MENU_ITEMS.size()];
|
||||
for (int i = 0; i < ROOT_ME_MENU_ITEMS.size(); i++) {
|
||||
titles[i] = ROOT_ME_MENU_ITEMS.get(i).title;
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
||||
public static String advancedEntryTitle() {
|
||||
return "高级与调试";
|
||||
public static MeMenuItem[] rootMeMenuItems() {
|
||||
return ROOT_ME_MENU_ITEMS.toArray(new MeMenuItem[0]);
|
||||
}
|
||||
|
||||
public static MeMenuItem findMeMenuItem(String key) {
|
||||
for (MeMenuItem item : ROOT_ME_MENU_ITEMS) {
|
||||
if (item.key.equals(key)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String[] projectQuickActions() {
|
||||
return PROJECT_QUICK_ACTIONS.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static Class<? extends BossScreenActivity> resolveConversationInfoTargetClass(boolean isGroup) {
|
||||
return isGroup ? GroupInfoActivity.class : ConversationInfoActivity.class;
|
||||
}
|
||||
|
||||
public static String[] projectPrimarySections() {
|
||||
return PROJECT_PRIMARY_SECTIONS.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static String buildSubtitle(JSONObject source) {
|
||||
String statusValue = source.optString("status", "");
|
||||
String status;
|
||||
if (source.optBoolean("online", false) || "online".equals(statusValue)) {
|
||||
status = "在线";
|
||||
} else if ("abnormal".equals(statusValue)) {
|
||||
status = "异常";
|
||||
} else {
|
||||
status = "离线";
|
||||
public static String conversationInfoActionLabel() {
|
||||
return "信息";
|
||||
}
|
||||
|
||||
public static String loginTitle() {
|
||||
return "会话";
|
||||
}
|
||||
|
||||
public static String loginHintText() {
|
||||
return "轻量会话首页已恢复,直接进入最近线程。";
|
||||
}
|
||||
|
||||
public static String loginButtonLabel() {
|
||||
return "进入会话";
|
||||
}
|
||||
|
||||
public static String conversationsHintPillText() {
|
||||
return "项目自动对应设备 GUI 项目文件夹";
|
||||
}
|
||||
|
||||
public static String conversationsHeaderSubtitle() {
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String conversationActivityIconMode() {
|
||||
return "animated_dots";
|
||||
}
|
||||
|
||||
public static int maxConversationActivityIcons() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static String conversationActivityAnimationCleanup() {
|
||||
return "cancel_on_detach";
|
||||
}
|
||||
|
||||
public static <T> T resolveRefreshValue(T cachedValue, T freshValue, boolean requestSucceeded) {
|
||||
if (requestSucceeded) {
|
||||
return freshValue;
|
||||
}
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
private static String buildSubtitle(JSONObject source) {
|
||||
String status = localizeDeviceStatus(resolveDeviceStatusKey(source));
|
||||
String account = source.optString("account", "");
|
||||
if (account.isEmpty()) {
|
||||
return status;
|
||||
@@ -99,6 +179,103 @@ public final class WechatSurfaceMapper {
|
||||
return status + " · " + account;
|
||||
}
|
||||
|
||||
private static String buildDeviceAccountProjectLine(JSONObject source) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String account = source.optString("account", "");
|
||||
if (!account.isEmpty()) {
|
||||
builder.append("账号: ").append(account);
|
||||
}
|
||||
String projects = joinProjects(source.optJSONArray("projects"));
|
||||
if (!projects.isEmpty()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append("项目: ").append(projects);
|
||||
}
|
||||
if (builder.length() == 0) {
|
||||
return buildSubtitle(source);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String buildDeviceQuotaStatusLine(JSONObject source) {
|
||||
StringBuilder builder = new StringBuilder("额度: ");
|
||||
int quota5h = source.optInt("quota5h", -1);
|
||||
int quota7d = source.optInt("quota7d", -1);
|
||||
boolean hasQuota = false;
|
||||
if (quota5h >= 0) {
|
||||
builder.append("5h ").append(quota5h).append("%");
|
||||
hasQuota = true;
|
||||
}
|
||||
if (quota7d >= 0) {
|
||||
if (hasQuota) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append("7d ").append(quota7d).append("%");
|
||||
hasQuota = true;
|
||||
}
|
||||
if (!hasQuota) {
|
||||
builder.append("暂无");
|
||||
}
|
||||
|
||||
String statusKey = resolveDeviceStatusKey(source);
|
||||
if ("abnormal".equals(statusKey)) {
|
||||
builder.append(" · 状态异常");
|
||||
} else if ("offline".equals(statusKey)) {
|
||||
builder.append(" · 当前离线");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String mergeDeviceMeta(String primary, String secondary) {
|
||||
if (primary == null || primary.isEmpty()) {
|
||||
return secondary;
|
||||
}
|
||||
if (secondary == null || secondary.isEmpty()) {
|
||||
return primary;
|
||||
}
|
||||
return primary + " · " + secondary;
|
||||
}
|
||||
|
||||
private static String joinProjects(JSONArray projects) {
|
||||
if (projects == null || projects.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < projects.length(); i++) {
|
||||
String project = projects.optString(i);
|
||||
if (project == null || project.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" / ");
|
||||
}
|
||||
builder.append(project);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String resolveDeviceStatusKey(JSONObject source) {
|
||||
String statusValue = source.optString("status", "");
|
||||
if (source.optBoolean("online", false) || "online".equals(statusValue)) {
|
||||
return "online";
|
||||
}
|
||||
if ("abnormal".equals(statusValue)) {
|
||||
return "abnormal";
|
||||
}
|
||||
return "offline";
|
||||
}
|
||||
|
||||
private static String localizeDeviceStatus(String statusKey) {
|
||||
if ("online".equals(statusKey)) {
|
||||
return "在线";
|
||||
}
|
||||
if ("abnormal".equals(statusKey)) {
|
||||
return "异常";
|
||||
}
|
||||
return "离线";
|
||||
}
|
||||
|
||||
private static String buildDetailMeta(JSONObject source) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendSegment(builder, source.optString("note", ""));
|
||||
@@ -132,26 +309,70 @@ public final class WechatSurfaceMapper {
|
||||
}
|
||||
|
||||
public static final class ConversationRow {
|
||||
public final String title;
|
||||
public final String preview;
|
||||
public final String threadTitle;
|
||||
public final String folderLabel;
|
||||
public final String lastMessagePreview;
|
||||
public final String timeLabel;
|
||||
public final int unreadCount;
|
||||
public final String topPinnedLabel;
|
||||
public final int activityIconCount;
|
||||
public final boolean isGroup;
|
||||
public final String avatarPrimary;
|
||||
public final String avatarSecondary;
|
||||
public final GroupAvatarMember[] groupAvatarMembers;
|
||||
|
||||
public ConversationRow(String title, String preview, String timeLabel, int unreadCount) {
|
||||
this.title = title;
|
||||
this.preview = preview;
|
||||
public ConversationRow(
|
||||
String threadTitle,
|
||||
String folderLabel,
|
||||
String lastMessagePreview,
|
||||
String timeLabel,
|
||||
int unreadCount,
|
||||
String topPinnedLabel,
|
||||
int activityIconCount,
|
||||
boolean isGroup,
|
||||
String avatarPrimary,
|
||||
String avatarSecondary,
|
||||
GroupAvatarMember[] groupAvatarMembers
|
||||
) {
|
||||
this.threadTitle = threadTitle;
|
||||
this.folderLabel = folderLabel;
|
||||
this.lastMessagePreview = lastMessagePreview;
|
||||
this.timeLabel = timeLabel;
|
||||
this.unreadCount = unreadCount;
|
||||
this.topPinnedLabel = topPinnedLabel;
|
||||
this.activityIconCount = activityIconCount;
|
||||
this.isGroup = isGroup;
|
||||
this.avatarPrimary = avatarPrimary;
|
||||
this.avatarSecondary = avatarSecondary;
|
||||
this.groupAvatarMembers = groupAvatarMembers == null ? new GroupAvatarMember[0] : groupAvatarMembers;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GroupAvatarMember {
|
||||
public final String threadId;
|
||||
public final String avatarLabel;
|
||||
public final String title;
|
||||
|
||||
public GroupAvatarMember(String threadId, String avatarLabel, String title) {
|
||||
this.threadId = threadId;
|
||||
this.avatarLabel = avatarLabel;
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DeviceRow {
|
||||
public final String title;
|
||||
public final String subtitle;
|
||||
public final String meta;
|
||||
public final String avatarLabel;
|
||||
public final String statusKey;
|
||||
|
||||
public DeviceRow(String title, String subtitle) {
|
||||
public DeviceRow(String title, String subtitle, String meta, String avatarLabel, String statusKey) {
|
||||
this.title = title;
|
||||
this.subtitle = subtitle;
|
||||
this.meta = meta;
|
||||
this.avatarLabel = avatarLabel;
|
||||
this.statusKey = statusKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,4 +387,16 @@ public final class WechatSurfaceMapper {
|
||||
this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MeMenuItem {
|
||||
public final String key;
|
||||
public final String title;
|
||||
public final String description;
|
||||
|
||||
public MeMenuItem(String key, String title, String description) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
android/app/src/main/res/layout/activity_conversation_info.xml
Normal file
109
android/app/src/main/res/layout/activity_conversation_info.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/boss_bg_app"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="返回"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="会话信息"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="单线程会话信息页"
|
||||
android:textColor="@color/boss_text_muted"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_header_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="操作"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="刷新"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/screen_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_panel"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="24dp" />
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
109
android/app/src/main/res/layout/activity_group_create.xml
Normal file
109
android/app/src/main/res/layout/activity_group_create.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/boss_bg_app"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="返回"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="发起群聊"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="从当前会话选择其他线程"
|
||||
android:textColor="@color/boss_text_muted"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_header_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="操作"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="刷新"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/screen_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_panel"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="24dp" />
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
109
android/app/src/main/res/layout/activity_group_info.xml
Normal file
109
android/app/src/main/res/layout/activity_group_info.xml
Normal file
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/boss_bg_app"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_surface"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_back_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="返回"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="群资料"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/screen_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:text="群聊资料页"
|
||||
android:textColor="@color/boss_text_muted"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_header_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="操作"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/screen_refresh_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="14dp"
|
||||
android:paddingRight="14dp"
|
||||
android:text="刷新"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/screen_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/screen_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_panel"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="24dp" />
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
@@ -16,25 +16,26 @@
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingTop="96dp"
|
||||
android:paddingTop="72dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingBottom="32dp">
|
||||
android:paddingBottom="40dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="@drawable/bg_tab_active"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:gravity="center"
|
||||
android:text="B"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textSize="32sp"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/login_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Boss 原生控制台"
|
||||
android:layout_marginTop="22dp"
|
||||
android:text=""
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="30sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -43,38 +44,12 @@
|
||||
android:id="@+id/login_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:text="原生 Android 客户端已启用。点击下方按钮直接进入系统。"
|
||||
android:text=""
|
||||
android:textColor="@color/boss_text_muted"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:background="@drawable/bg_list_row"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="当前临时模式"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:text="1. 这是原生 Android Activity,不再打开 WebView。\n2. 登录暂时不做验证,点击按钮会直接进入最高管理员会话。\n3. 会话 / 设备 / 我的三栏都直接调用现有 Boss API。"
|
||||
android:textColor="@color/boss_text_primary"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/login_progress"
|
||||
@@ -87,11 +62,11 @@
|
||||
android:id="@+id/login_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:background="@drawable/bg_primary_button"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="登录"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_surface"
|
||||
android:textSize="18sp"
|
||||
@@ -113,9 +88,9 @@
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingRight="20dp"
|
||||
android:paddingBottom="14dp">
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/back_button"
|
||||
@@ -153,9 +128,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="原生 Android 客户端,直接消费 Boss API。"
|
||||
android:text=""
|
||||
android:textColor="@color/boss_text_muted"
|
||||
android:textSize="13sp" />
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
@@ -163,11 +139,12 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_secondary_button"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="9dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="9dp"
|
||||
android:text="刷新"
|
||||
android:minWidth="0dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="刷"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/boss_green"
|
||||
android:textStyle="bold" />
|
||||
@@ -193,9 +170,11 @@
|
||||
android:id="@+id/screen_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/boss_panel"
|
||||
android:background="@color/boss_bg_app"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingBottom="88dp" />
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
public class AboutActivityStaleDownloadCleanupTest {
|
||||
@Test
|
||||
public void collectStaleDownloadIdsForRemoval_returnsIdsWhenReleaseChanged() throws Exception {
|
||||
JSONObject availableRelease = new StubJSONObject()
|
||||
.withString("packageFileName", "boss-android-v1.2.9-release.apk")
|
||||
.withString("version", "v1.2.9");
|
||||
|
||||
long[] ids = AboutActivity.collectStaleDownloadIdsForRemoval(
|
||||
availableRelease,
|
||||
"boss-android-v1.2.8-release.apk",
|
||||
"v1.2.8",
|
||||
true,
|
||||
42L,
|
||||
77L
|
||||
);
|
||||
|
||||
assertArrayEquals(new long[]{42L, 77L}, ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectStaleDownloadIdsForRemoval_returnsEmptyWhenReleaseMatchesLocalPackage() throws Exception {
|
||||
JSONObject availableRelease = new StubJSONObject()
|
||||
.withString("packageFileName", "boss-android-v1.2.9-release.apk")
|
||||
.withString("version", "v1.2.9");
|
||||
|
||||
long[] ids = AboutActivity.collectStaleDownloadIdsForRemoval(
|
||||
availableRelease,
|
||||
"boss-android-v1.2.9-release.apk",
|
||||
"v1.2.9",
|
||||
true,
|
||||
42L,
|
||||
77L
|
||||
);
|
||||
|
||||
assertArrayEquals(new long[0], ids);
|
||||
}
|
||||
|
||||
private static final class StubJSONObject extends JSONObject {
|
||||
private final java.util.Map<String, Object> values = new java.util.HashMap<>();
|
||||
|
||||
StubJSONObject withString(String key, String value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String optString(String key) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String optString(String key, String fallback) {
|
||||
String value = optString(key);
|
||||
return value.isEmpty() ? fallback : value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class GroupCreateActivityTest {
|
||||
@Test
|
||||
public void collectSelectableConversationItems_filtersOutExistingGroupChats() {
|
||||
JSONObject threadConversation = new StubJSONObject()
|
||||
.withString("projectId", "thread-1")
|
||||
.withString("projectTitle", "线程一")
|
||||
.withBoolean("isGroup", false);
|
||||
JSONObject groupConversation = new StubJSONObject()
|
||||
.withString("projectId", "group-1")
|
||||
.withString("projectTitle", "已有群聊")
|
||||
.withBoolean("isGroup", true);
|
||||
JSONObject sourceConversation = new StubJSONObject()
|
||||
.withString("projectId", "source-1")
|
||||
.withString("projectTitle", "来源线程")
|
||||
.withBoolean("isGroup", false);
|
||||
JSONObject conversationsPayload = new StubJSONObject()
|
||||
.withObjectArray("conversations", threadConversation, groupConversation, sourceConversation);
|
||||
|
||||
java.util.List<JSONObject> filtered = GroupCreateActivity.collectSelectableConversationItems(conversationsPayload, "source-1");
|
||||
|
||||
assertEquals(1, filtered.size());
|
||||
assertEquals("thread-1", filtered.get(0).optString("projectId", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reconcileSelectedProjectIds_keepsManualDeselectionWhenCandidatesStayTheSame() {
|
||||
Set<String> previousCandidateIds = linkedSet("thread-1", "thread-2", "thread-3");
|
||||
Set<String> currentSelectedIds = linkedSet("thread-1", "thread-3");
|
||||
Set<String> nextCandidateIds = linkedSet("thread-1", "thread-2", "thread-3");
|
||||
|
||||
Set<String> reconciled = GroupCreateActivity.reconcileSelectedProjectIds(
|
||||
currentSelectedIds,
|
||||
previousCandidateIds,
|
||||
nextCandidateIds
|
||||
);
|
||||
|
||||
assertEquals(2, reconciled.size());
|
||||
assertTrue(reconciled.contains("thread-1"));
|
||||
assertTrue(reconciled.contains("thread-3"));
|
||||
assertFalse(reconciled.contains("thread-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canCreateGroupChat_blocksWhileRefreshingOrCreating() {
|
||||
Set<String> selectedProjectIds = linkedSet("thread-1");
|
||||
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(true, false, selectedProjectIds));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, true, selectedProjectIds));
|
||||
assertTrue(GroupCreateActivity.canCreateGroupChat(false, false, selectedProjectIds));
|
||||
assertFalse(GroupCreateActivity.canCreateGroupChat(false, false, linkedSet()));
|
||||
}
|
||||
|
||||
private static Set<String> linkedSet(String... values) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (String value : values) {
|
||||
result.add(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class StubJSONObject extends JSONObject {
|
||||
private final java.util.Map<String, Object> values = new java.util.HashMap<>();
|
||||
|
||||
StubJSONObject withString(String key, String value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
StubJSONObject withBoolean(String key, boolean value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
StubJSONObject withObjectArray(String key, JSONObject... entries) {
|
||||
values.put(key, new StubJSONArray(entries));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String optString(String key, String defaultValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean optBoolean(String key, boolean defaultValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONArray optJSONArray(String key) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof JSONArray ? (JSONArray) value : null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubJSONArray extends JSONArray {
|
||||
private final JSONObject[] entries;
|
||||
|
||||
StubJSONArray(JSONObject... entries) {
|
||||
this.entries = entries == null ? new JSONObject[0] : entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject optJSONObject(int index) {
|
||||
if (index < 0 || index >= entries.length) {
|
||||
return null;
|
||||
}
|
||||
return entries[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,102 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WechatSurfaceMapperTest {
|
||||
@Test
|
||||
public void toConversationRow_keepsOnlyWechatFields() throws Exception {
|
||||
public void toConversationRow_mapsWechatConversationFieldsFromThreadPayload() throws Exception {
|
||||
JSONObject item = new StubJSONObject()
|
||||
.withString("projectTitle", "项目 A")
|
||||
.withString("preview", "最近消息预览")
|
||||
.withString("latestReplyLabel", "10:24")
|
||||
.withString("threadTitle", "北区试产线回归")
|
||||
.withString("folderLabel", "归档确认")
|
||||
.withString("preview", "旧预览")
|
||||
.withString("lastMessagePreview", "现场摄像头关键帧")
|
||||
.withString("latestReplyLabel", "09:26")
|
||||
.withInt("unreadCount", 3)
|
||||
.withString("deviceNamesPreview", "Mac Studio")
|
||||
.withBoolean("contextBudgetIndicator", true);
|
||||
.withInt("activityIconCount", 2)
|
||||
.withString("topPinnedLabel", "置顶")
|
||||
.withBoolean("isGroup", false)
|
||||
.withObject("avatar", new StubJSONObject()
|
||||
.withString("primary", "M")
|
||||
.withString("secondary", "W"))
|
||||
.withObjectArray("groupMembers",
|
||||
new StubJSONObject().withString("threadId", "t-1").withString("avatar", "M").withString("title", "Mac Studio"),
|
||||
new StubJSONObject().withString("threadId", "t-2").withString("avatar", "W").withString("title", "Windows GPU"));
|
||||
|
||||
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
|
||||
|
||||
assertEquals("项目 A", row.title);
|
||||
assertEquals("最近消息预览", row.preview);
|
||||
assertEquals("10:24", row.timeLabel);
|
||||
assertEquals("北区试产线回归", row.threadTitle);
|
||||
assertEquals("归档确认", row.folderLabel);
|
||||
assertEquals("现场摄像头关键帧", row.lastMessagePreview);
|
||||
assertEquals("09:26", row.timeLabel);
|
||||
assertEquals(3, row.unreadCount);
|
||||
assertEquals("置顶", row.topPinnedLabel);
|
||||
assertEquals(2, row.activityIconCount);
|
||||
assertFalse(row.isGroup);
|
||||
assertEquals("M", row.avatarPrimary);
|
||||
assertEquals("W", row.avatarSecondary);
|
||||
assertEquals(2, row.groupAvatarMembers.length);
|
||||
assertEquals("Mac Studio", row.groupAvatarMembers[0].title);
|
||||
assertEquals("W", row.groupAvatarMembers[1].avatarLabel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toDeviceRow_keepsOnlySimpleSubtitle() throws Exception {
|
||||
public void toConversationRow_prefersGroupMembersForGroupAvatarSummary() throws Exception {
|
||||
JSONObject item = new StubJSONObject()
|
||||
.withString("projectTitle", "群聊项目")
|
||||
.withString("threadTitle", "容灾切换验证")
|
||||
.withString("folderLabel", "Mac + Windows 协作")
|
||||
.withString("lastMessagePreview", "最新: API 切换记录回传")
|
||||
.withString("latestReplyLabel", "09:12")
|
||||
.withInt("activityIconCount", 2)
|
||||
.withString("topPinnedLabel", "置顶")
|
||||
.withBoolean("isGroup", true)
|
||||
.withObjectArray("groupMembers",
|
||||
new StubJSONObject().withString("threadId", "group-1").withString("avatar", "M").withString("title", "Mac Studio"),
|
||||
new StubJSONObject().withString("threadId", "group-2").withString("avatar", "W").withString("title", "Windows GPU"),
|
||||
new StubJSONObject().withString("threadId", "group-3").withString("avatar", "C").withString("title", "Cloud Backup"));
|
||||
|
||||
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
|
||||
|
||||
assertTrue(row.isGroup);
|
||||
assertEquals("容灾切换验证", row.threadTitle);
|
||||
assertEquals("Mac + Windows 协作", row.folderLabel);
|
||||
assertEquals("最新: API 切换记录回传", row.lastMessagePreview);
|
||||
assertEquals("09:12", row.timeLabel);
|
||||
assertEquals(3, row.groupAvatarMembers.length);
|
||||
assertEquals("M", row.groupAvatarMembers[0].avatarLabel);
|
||||
assertEquals("Cloud Backup", row.groupAvatarMembers[2].title);
|
||||
assertEquals("", row.avatarPrimary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toDeviceRow_mapsLegacyWechatThreeLineSummary() throws Exception {
|
||||
JSONObject item = new StubJSONObject()
|
||||
.withString("name", "Mac Studio")
|
||||
.withString("avatar", "M")
|
||||
.withString("status", "online")
|
||||
.withString("account", "17600003315")
|
||||
.withStringArray("projects", "北区试产线回归", "容灾切换验证")
|
||||
.withInt("quota5h", 8)
|
||||
.withInt("quota7d", 22);
|
||||
|
||||
WechatSurfaceMapper.DeviceRow row = WechatSurfaceMapper.toDeviceRow(item);
|
||||
|
||||
assertEquals("Mac Studio", row.title);
|
||||
assertEquals("在线 · 17600003315", row.subtitle);
|
||||
assertEquals("账号: 17600003315 · 项目: 北区试产线回归 / 容灾切换验证", row.subtitle);
|
||||
assertEquals("额度: 5h 8% · 7d 22%", row.meta);
|
||||
assertEquals("M", row.avatarLabel);
|
||||
assertEquals("online", row.statusKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -50,7 +109,9 @@ public class WechatSurfaceMapperTest {
|
||||
WechatSurfaceMapper.DeviceRow row = WechatSurfaceMapper.toDeviceRow(item);
|
||||
|
||||
assertEquals("Mac Studio", row.title);
|
||||
assertEquals("异常 · 17600003315", row.subtitle);
|
||||
assertEquals("账号: 17600003315", row.subtitle);
|
||||
assertEquals("额度: 暂无 · 状态异常", row.meta);
|
||||
assertEquals("abnormal", row.statusKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,19 +122,19 @@ public class WechatSurfaceMapperTest {
|
||||
.withString("account", "17600003315")
|
||||
.withString("note", "书房主机")
|
||||
.withString("endpoint", "https://boss.hyzq.net/device/mac-studio")
|
||||
.withArray("projects", "master-agent", "android-app");
|
||||
.withStringArray("projects", "master-agent", "android-app");
|
||||
|
||||
WechatSurfaceMapper.DeviceDetailSummary summary = WechatSurfaceMapper.toDeviceDetailSummary(item);
|
||||
|
||||
assertEquals("Mac Studio", summary.title);
|
||||
assertEquals("在线 · 17600003315", summary.subtitle);
|
||||
assertEquals("书房主机 · https://boss.hyzq.net/device/mac-studio · 项目 master-agent, android-app", summary.meta);
|
||||
assertEquals("账号: 17600003315 · 项目: master-agent / android-app", summary.subtitle);
|
||||
assertEquals("额度: 暂无 · 书房主机 · https://boss.hyzq.net/device/mac-studio · 项目 master-agent, android-app", summary.meta);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootMeMenuTitles_matchApprovedSimpleMenu() throws Exception {
|
||||
public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception {
|
||||
assertArrayEquals(
|
||||
new String[]{"账号与安全", "AI 账号", "设置", "技能", "关于"},
|
||||
new String[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
|
||||
WechatSurfaceMapper.rootMeMenuTitles()
|
||||
);
|
||||
}
|
||||
@@ -87,16 +148,112 @@ public class WechatSurfaceMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mainPage_doesNotExposeOpsEntry() throws Exception {
|
||||
public void mainPage_keepsOpsEntryInStableWechatMenuOrder() throws Exception {
|
||||
assertArrayEquals(
|
||||
new String[]{"账号与安全", "AI 账号", "设置", "技能", "关于"},
|
||||
new String[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
|
||||
WechatSurfaceMapper.rootMeMenuTitles()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advancedEntryTitle_movesOpsOutOfMainMePage() throws Exception {
|
||||
assertEquals("高级与调试", WechatSurfaceMapper.advancedEntryTitle());
|
||||
public void opsEntryCopy_staysInMeFlowWithoutLegacyAdvancedEntrySemantics() throws Exception {
|
||||
WechatSurfaceMapper.MeMenuItem opsItem = WechatSurfaceMapper.findMeMenuItem("ops");
|
||||
|
||||
assertNotNull(opsItem);
|
||||
assertEquals("运维与修复", opsItem.title);
|
||||
assertEquals("查看运维会话、修复回放与 standby 切换", opsItem.description);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void meFlow_doesNotExposeAuditConversationCopy() throws Exception {
|
||||
for (WechatSurfaceMapper.MeMenuItem item : WechatSurfaceMapper.rootMeMenuItems()) {
|
||||
assertFalse(item.title.contains("审计"));
|
||||
assertFalse(item.description.contains("审计"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aboutActivity_parsesStructuredOtaSummaryArrayIntoReadableContent() throws Exception {
|
||||
JSONObject ota = new StubJSONObject()
|
||||
.withObject("availableRelease", new StubJSONObject()
|
||||
.withString("version", "v1.2.8")
|
||||
.withStringArray("summary", "优化设备状态刷新", "修复主 Agent 会话排序", "提升 OTA 回收稳定性"));
|
||||
|
||||
java.lang.reflect.Method method = AboutActivity.class.getDeclaredMethod("buildOtaContentBody", JSONObject.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String content = (String) method.invoke(null, ota);
|
||||
|
||||
assertEquals("版本 v1.2.8\n1. 优化设备状态刷新\n2. 修复主 Agent 会话排序\n3. 提升 OTA 回收稳定性", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aboutActivity_rejectsStaleDownloadedApkWhenAvailableReleaseChanged() throws Exception {
|
||||
JSONObject availableRelease = new StubJSONObject()
|
||||
.withString("version", "v1.2.9")
|
||||
.withString("packageFileName", "boss-android-v1.2.9-release.apk");
|
||||
|
||||
java.lang.reflect.Method method = AboutActivity.class.getDeclaredMethod(
|
||||
"isDownloadedReleaseCurrent",
|
||||
JSONObject.class,
|
||||
String.class,
|
||||
String.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
|
||||
boolean stillCurrent = (Boolean) method.invoke(
|
||||
null,
|
||||
availableRelease,
|
||||
"boss-android-v1.2.8-release.apk",
|
||||
"v1.2.8"
|
||||
);
|
||||
|
||||
assertFalse(stillCurrent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skillInventory_fallsBackWhenExplicitDeviceIdIsInvalid() throws Exception {
|
||||
JSONArray devices = new StubObjectArray(
|
||||
new StubJSONObject()
|
||||
.withString("id", "device-b")
|
||||
.withString("account", "17600003315"),
|
||||
new StubJSONObject()
|
||||
.withString("id", "device-c")
|
||||
.withString("account", "other-account")
|
||||
);
|
||||
|
||||
java.lang.reflect.Method method = SkillInventoryActivity.class.getDeclaredMethod(
|
||||
"chooseTargetDeviceId",
|
||||
String.class,
|
||||
String.class,
|
||||
String.class,
|
||||
JSONArray.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
|
||||
String resolved = (String) method.invoke(
|
||||
null,
|
||||
"stale-device-id",
|
||||
"missing-bound-device",
|
||||
"17600003315",
|
||||
devices
|
||||
);
|
||||
|
||||
assertEquals("device-b", resolved);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aboutActivity_collectsAllPositiveDownloadIdsForStaleRemoval() throws Exception {
|
||||
java.lang.reflect.Method method = AboutActivity.class.getDeclaredMethod(
|
||||
"collectDownloadIdsForRemoval",
|
||||
long.class,
|
||||
long.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
|
||||
long[] ids = (long[]) method.invoke(null, 42L, 77L);
|
||||
|
||||
assertArrayEquals(new long[]{42L, 77L}, ids);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,6 +264,16 @@ public class WechatSurfaceMapperTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectDetailInfoTarget_routesSingleThreadsToConversationInfo() {
|
||||
assertEquals(ConversationInfoActivity.class, WechatSurfaceMapper.resolveConversationInfoTargetClass(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectDetailInfoTarget_routesGroupChatsToGroupInfo() {
|
||||
assertEquals(GroupInfoActivity.class, WechatSurfaceMapper.resolveConversationInfoTargetClass(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectPrimarySections_keepOnlyChatEssentials() throws Exception {
|
||||
assertArrayEquals(
|
||||
@@ -115,6 +282,66 @@ public class WechatSurfaceMapperTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conversationsChrome_copyStaysLightweightInsteadOfConsoleInstructions() throws Exception {
|
||||
assertEquals("会话", WechatSurfaceMapper.loginTitle());
|
||||
assertEquals("进入会话", WechatSurfaceMapper.loginButtonLabel());
|
||||
assertEquals("项目自动对应设备 GUI 项目文件夹", WechatSurfaceMapper.conversationsHintPillText());
|
||||
assertEquals("", WechatSurfaceMapper.conversationsHeaderSubtitle());
|
||||
assertFalse(WechatSurfaceMapper.loginHintText().contains("Boss API"));
|
||||
assertFalse(WechatSurfaceMapper.loginHintText().contains("控制台"));
|
||||
assertFalse(WechatSurfaceMapper.loginHintText().contains("数据仍来自现有 Boss API"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conversationActivityIcons_useAnimatedDotViewsInsteadOfTextGlyphs() throws Exception {
|
||||
assertEquals("animated_dots", WechatSurfaceMapper.conversationActivityIconMode());
|
||||
assertEquals(4, WechatSurfaceMapper.maxConversationActivityIcons());
|
||||
assertEquals("cancel_on_detach", WechatSurfaceMapper.conversationActivityAnimationCleanup());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void meMenuItems_useStableKeysInsteadOfDisplayTitlesForRouting() throws Exception {
|
||||
WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems();
|
||||
|
||||
assertEquals(6, items.length);
|
||||
assertEquals("security", items[0].key);
|
||||
assertEquals("账号与安全", items[0].title);
|
||||
assertEquals("settings", items[1].key);
|
||||
assertEquals("ops", items[2].key);
|
||||
assertEquals("运维与修复", items[2].title);
|
||||
assertEquals("ai_accounts", items[3].key);
|
||||
assertEquals("skills", items[4].key);
|
||||
assertEquals("about", items[5].key);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshMergePolicy_appliesSuccessfulPayloadsWithoutDroppingCachedValues() throws Exception {
|
||||
JSONArray cachedConversations = new StubStringArray("cached-conversation");
|
||||
JSONArray freshConversations = new StubStringArray("fresh-conversation");
|
||||
JSONArray cachedDevices = new StubStringArray("cached-device");
|
||||
JSONObject cachedOta = new StubJSONObject().withString("version", "1.0.0");
|
||||
JSONObject freshSettings = new StubJSONObject().withString("preferredEntryPoint", "devices");
|
||||
|
||||
assertSame(
|
||||
freshConversations,
|
||||
WechatSurfaceMapper.resolveRefreshValue(cachedConversations, freshConversations, true)
|
||||
);
|
||||
assertSame(
|
||||
cachedDevices,
|
||||
WechatSurfaceMapper.resolveRefreshValue(cachedDevices, new StubStringArray("fresh-device"), false)
|
||||
);
|
||||
assertSame(
|
||||
cachedOta,
|
||||
WechatSurfaceMapper.resolveRefreshValue(cachedOta, new StubJSONObject().withString("version", "2.0.0"), false)
|
||||
);
|
||||
assertSame(
|
||||
freshSettings,
|
||||
WechatSurfaceMapper.resolveRefreshValue(null, freshSettings, true)
|
||||
);
|
||||
assertNull(WechatSurfaceMapper.resolveRefreshValue(null, null, false));
|
||||
}
|
||||
|
||||
private static final class StubJSONObject extends JSONObject {
|
||||
private final java.util.Map<String, Object> values = new java.util.HashMap<>();
|
||||
|
||||
@@ -133,8 +360,18 @@ public class WechatSurfaceMapperTest {
|
||||
return this;
|
||||
}
|
||||
|
||||
StubJSONObject withArray(String key, String... entries) {
|
||||
values.put(key, new StubJSONArray(entries));
|
||||
StubJSONObject withObject(String key, JSONObject value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
StubJSONObject withStringArray(String key, String... entries) {
|
||||
values.put(key, new StubStringArray(entries));
|
||||
return this;
|
||||
}
|
||||
|
||||
StubJSONObject withObjectArray(String key, JSONObject... entries) {
|
||||
values.put(key, new StubObjectArray(entries));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -157,16 +394,22 @@ public class WechatSurfaceMapperTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.json.JSONArray optJSONArray(String key) {
|
||||
public JSONObject optJSONObject(String key) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof org.json.JSONArray ? (org.json.JSONArray) value : null;
|
||||
return value instanceof JSONObject ? (JSONObject) value : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONArray optJSONArray(String key) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof JSONArray ? (JSONArray) value : null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubJSONArray extends org.json.JSONArray {
|
||||
private static final class StubStringArray extends JSONArray {
|
||||
private final String[] entries;
|
||||
|
||||
StubJSONArray(String... entries) {
|
||||
StubStringArray(String... entries) {
|
||||
this.entries = entries == null ? new String[0] : entries;
|
||||
}
|
||||
|
||||
@@ -180,8 +423,28 @@ public class WechatSurfaceMapperTest {
|
||||
if (index < 0 || index >= entries.length) {
|
||||
return "";
|
||||
}
|
||||
String value = entries[index];
|
||||
return value == null ? "" : value;
|
||||
return entries[index] == null ? "" : entries[index];
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubObjectArray extends JSONArray {
|
||||
private final JSONObject[] entries;
|
||||
|
||||
StubObjectArray(JSONObject... entries) {
|
||||
this.entries = entries == null ? new JSONObject[0] : entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject optJSONObject(int index) {
|
||||
if (index < 0 || index >= entries.length) {
|
||||
return null;
|
||||
}
|
||||
return entries[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user