chore: publish native ui polish release v2.5.2
This commit is contained in:
@@ -36,8 +36,8 @@ android {
|
||||
applicationId "com.hyzq.boss"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 14
|
||||
versionName "2.5.1"
|
||||
versionCode 15
|
||||
versionName "2.5.2"
|
||||
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("会话信息", projectName == null ? "单线程会话信息页" : projectName);
|
||||
setHeaderAction("重命名", v -> openRenameDialog());
|
||||
configureScreen("会话信息", projectName == null ? "单线程会话" : projectName);
|
||||
setHeaderAction("改名", v -> openRenameDialog());
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -76,30 +76,49 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
participantCount = participants == null ? 0 : participants.length();
|
||||
configureScreen("会话信息", buildSubtitle(threadMeta, participantCount));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
projectName,
|
||||
buildDetailBody(project, threadMeta),
|
||||
buildDetailMeta(projectId, projectFolderName, participantCount)
|
||||
"单线程会话",
|
||||
buildHeaderDetail(project, threadMeta, participantCount)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildMenuRow(
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"发起群聊",
|
||||
"从当前会话选择其他线程,创建新的独立群聊",
|
||||
"选择其他线程加入新群",
|
||||
"原会话保留",
|
||||
null,
|
||||
v -> openGroupCreate()
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"参与设备 / 线程",
|
||||
"以下线程参与当前会话,点击可查看对应项目详情。",
|
||||
participantCount == 0 ? "当前没有可展示的参与线程。" : "共 " + participantCount + " 个参与线程"
|
||||
"线程详情",
|
||||
"查看当前线程聊天与项目",
|
||||
resolveThreadId(project, threadMeta),
|
||||
null,
|
||||
v -> openProject(projectId, projectName)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"参与线程",
|
||||
participantCount <= 0 ? "暂无参与线程" : "共 " + participantCount + " 个",
|
||||
projectFolderName.isEmpty() ? null : projectFolderName,
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
if (participants == null || participants.length() == 0) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "当前没有参与线程信息。"));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"暂无参与线程",
|
||||
"下拉刷新后重试",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
} else {
|
||||
for (int i = 0; i < participants.length(); i++) {
|
||||
JSONObject participant = participants.optJSONObject(i);
|
||||
@@ -120,7 +139,10 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
if (!participant.optString("threadId", "").isEmpty()) {
|
||||
meta = meta.isEmpty() ? participant.optString("threadId", "") : meta + " · " + participant.optString("threadId", "");
|
||||
}
|
||||
return BossUi.buildListRow(
|
||||
if (sourceProject) {
|
||||
subtitle = subtitle.isEmpty() ? "来源线程" : "来源线程 · " + subtitle;
|
||||
}
|
||||
return BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
title,
|
||||
subtitle,
|
||||
@@ -198,33 +220,32 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
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());
|
||||
private String buildHeaderDetail(JSONObject project, @Nullable JSONObject threadMeta, int count) {
|
||||
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);
|
||||
String threadId = resolveThreadId(project, threadMeta);
|
||||
if (!threadId.isEmpty()) {
|
||||
builder.append(threadId);
|
||||
}
|
||||
if (!folderName.isEmpty()) {
|
||||
if (!projectFolderName.isEmpty()) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(folderName);
|
||||
builder.append(projectFolderName);
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(count <= 0 ? "暂无参与线程" : "参与线程 " + count);
|
||||
builder.append(count <= 0 ? "暂无参与线程" : count + " 个参与线程");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String resolveThreadId(JSONObject project, @Nullable JSONObject threadMeta) {
|
||||
if (threadMeta != null) {
|
||||
String threadId = threadMeta.optString("threadId", "");
|
||||
if (!threadId.isEmpty()) {
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
return project.optString("id", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("群资料", projectName == null ? "群聊资料页" : projectName);
|
||||
setHeaderAction("重命名", v -> openRenameDialog());
|
||||
configureScreen("群资料", projectName == null ? "协作群聊" : projectName);
|
||||
setHeaderAction("改名", v -> openRenameDialog());
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -74,22 +74,40 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
int participantCount = participants == null ? 0 : participants.length();
|
||||
configureScreen("群资料", buildSubtitle(folderName, participantCount));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
projectName,
|
||||
buildDetailBody(project, threadMeta),
|
||||
buildDetailMeta(projectId, folderName, participantCount)
|
||||
"协作群聊",
|
||||
buildHeaderDetail(project, threadMeta, folderName, participantCount)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildCard(
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"成员线程",
|
||||
"群聊成员可点击查看对应项目详情。",
|
||||
participantCount == 0 ? "当前没有成员线程。" : "共 " + participantCount + " 个成员"
|
||||
"线程详情",
|
||||
"查看当前群聊对应项目",
|
||||
resolveThreadId(project, threadMeta),
|
||||
null,
|
||||
v -> openProject(projectId, projectName)
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"群成员",
|
||||
participantCount <= 0 ? "暂无成员" : "共 " + participantCount + " 个",
|
||||
folderName.isEmpty() ? null : folderName,
|
||||
null,
|
||||
null
|
||||
));
|
||||
|
||||
if (participants == null || participants.length() == 0) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "当前没有群成员信息。"));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"暂无群成员",
|
||||
"下拉刷新后重试",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
} else {
|
||||
for (int i = 0; i < participants.length(); i++) {
|
||||
JSONObject participant = participants.optJSONObject(i);
|
||||
@@ -111,7 +129,10 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
if (!threadId.isEmpty()) {
|
||||
meta = meta.isEmpty() ? threadId : meta + " · " + threadId;
|
||||
}
|
||||
return BossUi.buildListRow(
|
||||
if (sourceProject) {
|
||||
subtitle = subtitle.isEmpty() ? "当前群聊" : "当前群聊 · " + subtitle;
|
||||
}
|
||||
return BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
title,
|
||||
subtitle,
|
||||
@@ -177,21 +198,11 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
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", "");
|
||||
private String buildHeaderDetail(JSONObject project, @Nullable JSONObject threadMeta, String folderName, int count) {
|
||||
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);
|
||||
String threadId = resolveThreadId(project, threadMeta);
|
||||
if (!threadId.isEmpty()) {
|
||||
builder.append(threadId);
|
||||
}
|
||||
if (!folderName.isEmpty()) {
|
||||
if (builder.length() > 0) {
|
||||
@@ -202,7 +213,17 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(count <= 0 ? "暂无成员" : "成员 " + count);
|
||||
builder.append(count <= 0 ? "暂无成员" : count + " 个成员");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String resolveThreadId(JSONObject project, @Nullable JSONObject threadMeta) {
|
||||
if (threadMeta != null) {
|
||||
String threadId = threadMeta.optString("threadId", "");
|
||||
if (!threadId.isEmpty()) {
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
return project.optString("id", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
@@ -611,24 +612,217 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
: ProjectChatUiState.formatAttachmentSize(attachment.optLong("fileSizeBytes", 0L));
|
||||
String analysisState = attachment.optString("analysisState", "");
|
||||
String attachmentId = attachment.optString("attachmentId", "");
|
||||
String actionLabel = ProjectChatUiState.actionLabelForAttachmentAnalysisState(analysisState);
|
||||
String summary = attachment.optString("analysisSummary", "");
|
||||
String actionLabel = mapAttachmentAnalysisActionLabel(analysisState);
|
||||
String statusLabel = mapAttachmentAnalysisStatusLabel(analysisState, summary);
|
||||
View.OnClickListener actionListener = TextUtils.isEmpty(actionLabel) || TextUtils.isEmpty(attachmentId)
|
||||
? null
|
||||
: v -> requestAttachmentAnalysis(attachmentId, attachment.optString("fileName", "附件"));
|
||||
return BossUi.buildAttachmentMessageCard(
|
||||
View attachmentView = BossUi.buildAttachmentMessageCard(
|
||||
this,
|
||||
senderLabel,
|
||||
sourceType,
|
||||
attachment.optString("fileName", "attachment"),
|
||||
detail,
|
||||
ProjectChatUiState.labelForAttachmentAnalysisState(analysisState),
|
||||
attachment.optString("analysisSummary", ""),
|
||||
statusLabel,
|
||||
summary,
|
||||
actionLabel,
|
||||
actionListener,
|
||||
meta,
|
||||
outgoing,
|
||||
null
|
||||
);
|
||||
refineAttachmentMessageCard(attachmentView, analysisState, summary, actionLabel, actionListener);
|
||||
return attachmentView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String mapAttachmentAnalysisStatusLabel(@Nullable String analysisState, @Nullable String summary) {
|
||||
if ("ready_manual".equals(analysisState)) {
|
||||
return "可分析";
|
||||
}
|
||||
if ("completed".equals(analysisState) && !TextUtils.isEmpty(summary)) {
|
||||
return null;
|
||||
}
|
||||
return ProjectChatUiState.labelForAttachmentAnalysisState(analysisState);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String mapAttachmentAnalysisActionLabel(@Nullable String analysisState) {
|
||||
if ("ready_manual".equals(analysisState)) {
|
||||
return "AI 分析";
|
||||
}
|
||||
if ("failed".equals(analysisState)) {
|
||||
return "重试";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void refineAttachmentMessageCard(
|
||||
View attachmentView,
|
||||
@Nullable String analysisState,
|
||||
@Nullable String summary,
|
||||
@Nullable String actionLabel,
|
||||
@Nullable View.OnClickListener actionListener
|
||||
) {
|
||||
boolean hasSummary = !TextUtils.isEmpty(summary);
|
||||
refineAttachmentCardViews(attachmentView, analysisState, hasSummary);
|
||||
if (!TextUtils.isEmpty(actionLabel) && actionListener != null) {
|
||||
ensureCompactAttachmentAction(attachmentView, actionLabel, actionListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void refineAttachmentCardViews(
|
||||
@Nullable View root,
|
||||
@Nullable String analysisState,
|
||||
boolean hasSummary
|
||||
) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
if (root instanceof Button) {
|
||||
compactAttachmentActionButton((Button) root);
|
||||
} else if (root instanceof TextView) {
|
||||
compactAttachmentTextView((TextView) root, analysisState, hasSummary);
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
refineAttachmentCardViews(group.getChildAt(i), analysisState, hasSummary);
|
||||
}
|
||||
}
|
||||
|
||||
private void compactAttachmentActionButton(Button button) {
|
||||
button.setMinWidth(BossUi.dp(this, 64));
|
||||
button.setTextSize(12);
|
||||
button.setPadding(BossUi.dp(this, 12), 0, BossUi.dp(this, 12), 0);
|
||||
ViewGroup.LayoutParams layoutParams = button.getLayoutParams();
|
||||
if (layoutParams instanceof LinearLayout.LayoutParams) {
|
||||
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) layoutParams;
|
||||
params.topMargin = BossUi.dp(this, 8);
|
||||
button.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
private void compactAttachmentTextView(
|
||||
TextView textView,
|
||||
@Nullable String analysisState,
|
||||
boolean hasSummary
|
||||
) {
|
||||
CharSequence rawText = textView.getText();
|
||||
if (rawText == null) {
|
||||
return;
|
||||
}
|
||||
String text = rawText.toString();
|
||||
if ("completed".equals(analysisState) && hasSummary) {
|
||||
if ("已分析".equals(text)) {
|
||||
textView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
if (text.contains(" · 已分析")) {
|
||||
textView.setText(text.replace(" · 已分析", ""));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ("queued_auto".equals(analysisState)) {
|
||||
replaceAttachmentText(textView, text, "自动分析排队中", "AI 排队中");
|
||||
return;
|
||||
}
|
||||
if ("ready_manual".equals(analysisState)) {
|
||||
replaceAttachmentText(textView, text, "待分析", "可分析");
|
||||
return;
|
||||
}
|
||||
if ("processing".equals(analysisState)) {
|
||||
replaceAttachmentText(textView, text, "AI 分析中", "AI 处理中");
|
||||
return;
|
||||
}
|
||||
if ("failed".equals(analysisState)) {
|
||||
replaceAttachmentText(textView, text, "分析失败", "可重试");
|
||||
}
|
||||
}
|
||||
|
||||
private void replaceAttachmentText(
|
||||
TextView textView,
|
||||
String currentText,
|
||||
String from,
|
||||
String to
|
||||
) {
|
||||
if (from.equals(currentText)) {
|
||||
textView.setText(to);
|
||||
return;
|
||||
}
|
||||
String suffixed = " · " + from;
|
||||
if (currentText.contains(suffixed)) {
|
||||
textView.setText(currentText.replace(suffixed, " · " + to));
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureCompactAttachmentAction(
|
||||
View attachmentView,
|
||||
String actionLabel,
|
||||
View.OnClickListener actionListener
|
||||
) {
|
||||
Button existingButton = findAttachmentActionButton(attachmentView);
|
||||
if (existingButton != null) {
|
||||
existingButton.setText(actionLabel);
|
||||
existingButton.setOnClickListener(actionListener);
|
||||
compactAttachmentActionButton(existingButton);
|
||||
return;
|
||||
}
|
||||
LinearLayout card = findAttachmentCardLayout(attachmentView);
|
||||
if (card == null) {
|
||||
return;
|
||||
}
|
||||
Button actionButton = new Button(this);
|
||||
actionButton.setText(actionLabel);
|
||||
actionButton.setAllCaps(false);
|
||||
actionButton.setBackgroundResource(R.drawable.bg_secondary_button);
|
||||
actionButton.setTextColor(getColor(R.color.boss_text_primary));
|
||||
actionButton.setOnClickListener(actionListener);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
BossUi.dp(this, 32)
|
||||
);
|
||||
params.topMargin = BossUi.dp(this, 8);
|
||||
actionButton.setLayoutParams(params);
|
||||
compactAttachmentActionButton(actionButton);
|
||||
card.addView(actionButton);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Button findAttachmentActionButton(@Nullable View root) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
if (root instanceof Button) {
|
||||
return (Button) root;
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return null;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
Button found = findAttachmentActionButton(group.getChildAt(i));
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LinearLayout findAttachmentCardLayout(@Nullable View attachmentView) {
|
||||
if (!(attachmentView instanceof LinearLayout)) {
|
||||
return null;
|
||||
}
|
||||
LinearLayout wrapper = (LinearLayout) attachmentView;
|
||||
if (wrapper.getChildCount() < 2) {
|
||||
return null;
|
||||
}
|
||||
View card = wrapper.getChildAt(1);
|
||||
return card instanceof LinearLayout ? (LinearLayout) card : null;
|
||||
}
|
||||
|
||||
private void bindMessageInteractions(
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.hyzq.boss;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.view.Gravity;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@@ -23,7 +25,7 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("项目目标", projectName == null ? "原生目标清单" : projectName);
|
||||
setHeaderAction("新增", v -> openGoalEditor(null, ""));
|
||||
setHeaderAction("编辑目标", v -> openGoalEditor(null, ""));
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -63,11 +65,12 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
int goalCount = goals == null ? 0 : goals.length();
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"主 Agent 已整理项目目标",
|
||||
"已完成 " + completedCount + "/" + (goals == null ? 0 : goals.length()),
|
||||
"用户可编辑,点按钮即可标记完成或修改正文。"
|
||||
"主 Agent 已整理项目目标 · 已完成 " + completedCount + "/" + goalCount,
|
||||
"最近更新 09:18 · 用户可编辑,点选圆圈标记完成后自动划线",
|
||||
""
|
||||
));
|
||||
|
||||
if (goals == null || goals.length() == 0) {
|
||||
@@ -76,7 +79,7 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
for (int i = 0; i < goals.length(); i++) {
|
||||
JSONObject goal = goals.optJSONObject(i);
|
||||
if (goal == null) continue;
|
||||
appendContent(buildGoalCard(goal));
|
||||
appendContent(buildGoalChecklistCard(goal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,29 +87,66 @@ public class ProjectGoalsActivity extends BossScreenActivity {
|
||||
this,
|
||||
"当前约束",
|
||||
"• 只能使用已绑定设备\n• 审计证据必须可回放\n• 版本记录仅主 Agent 可发布",
|
||||
"原生目标页已覆盖 Web 目标清单"
|
||||
""
|
||||
));
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildGoalCard(JSONObject goal) {
|
||||
LinearLayout card = BossUi.buildCard(
|
||||
this,
|
||||
goal.optString("text", "未命名目标"),
|
||||
goal.optString("note", "暂无备注"),
|
||||
"状态 " + goal.optString("state", "pending")
|
||||
);
|
||||
private LinearLayout buildGoalChecklistCard(JSONObject goal) {
|
||||
LinearLayout card = BossUi.buildCard(this, "", "", "");
|
||||
card.removeAllViews();
|
||||
card.setClickable(true);
|
||||
card.setFocusable(true);
|
||||
card.setOnClickListener(v -> toggleGoal(goal.optString("id")));
|
||||
card.setOnLongClickListener(v -> {
|
||||
openGoalEditor(goal.optString("id"), goal.optString("text"));
|
||||
return true;
|
||||
});
|
||||
|
||||
Button toggle = BossUi.buildPrimaryButton(
|
||||
this,
|
||||
"completed".equals(goal.optString("state")) ? "标记未完成" : "标记完成"
|
||||
);
|
||||
toggle.setOnClickListener(v -> toggleGoal(goal.optString("id")));
|
||||
card.addView(toggle);
|
||||
boolean completed = "completed".equals(goal.optString("state"));
|
||||
|
||||
Button edit = BossUi.buildSecondaryButton(this, "编辑目标");
|
||||
edit.setOnClickListener(v -> openGoalEditor(goal.optString("id"), goal.optString("text")));
|
||||
card.addView(edit);
|
||||
LinearLayout row = new LinearLayout(this);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
row.setGravity(Gravity.TOP);
|
||||
|
||||
TextView indicator = new TextView(this);
|
||||
LinearLayout.LayoutParams indicatorParams = new LinearLayout.LayoutParams(
|
||||
BossUi.dp(this, 28),
|
||||
BossUi.dp(this, 28)
|
||||
);
|
||||
indicatorParams.rightMargin = BossUi.dp(this, 12);
|
||||
indicator.setLayoutParams(indicatorParams);
|
||||
indicator.setGravity(Gravity.CENTER);
|
||||
indicator.setText(completed ? "✓" : "○");
|
||||
indicator.setTextSize(18);
|
||||
indicator.setTextColor(getColor(completed ? R.color.boss_green : R.color.boss_text_muted));
|
||||
row.addView(indicator);
|
||||
|
||||
LinearLayout texts = new LinearLayout(this);
|
||||
texts.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
1f
|
||||
);
|
||||
texts.setLayoutParams(textParams);
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setText(goal.optString("text", "未命名目标"));
|
||||
title.setTextSize(16);
|
||||
title.setTextColor(getColor(R.color.boss_text_primary));
|
||||
title.setLineSpacing(0f, 1.2f);
|
||||
texts.addView(title);
|
||||
|
||||
TextView note = new TextView(this);
|
||||
note.setText(goal.optString("note", "暂无备注"));
|
||||
note.setTextSize(13);
|
||||
note.setTextColor(getColor(R.color.boss_text_muted));
|
||||
note.setPadding(0, BossUi.dp(this, 8), 0, 0);
|
||||
texts.addView(note);
|
||||
|
||||
row.addView(texts);
|
||||
card.addView(row);
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
configureScreen("版本迭代记录", getIntent().getStringExtra(EXTRA_PROJECT_NAME));
|
||||
setHeaderAction("只读", v -> showMessage("版本记录只读"));
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -41,9 +42,9 @@ public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
private void renderVersions(@Nullable JSONObject project) {
|
||||
replaceContent(BossUi.buildCard(
|
||||
this,
|
||||
"版本记录只读",
|
||||
"版本记录由主 Agent 监督各线程提交,并在复核后自动发布。",
|
||||
"原生版本页仅展示,不允许手工篡改正文。"
|
||||
"仅主 Agent 可发布迭代记录",
|
||||
"每条记录需备核线程提交内容、测试结论与版本号一致性。",
|
||||
""
|
||||
));
|
||||
if (project == null) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "项目不存在。"));
|
||||
@@ -63,9 +64,18 @@ public class ProjectVersionsActivity extends BossScreenActivity {
|
||||
this,
|
||||
item.optString("version", "未命名版本"),
|
||||
item.optString("summary", ""),
|
||||
item.optString("createdAt", "-")
|
||||
""
|
||||
));
|
||||
}
|
||||
String reviewTime = versions.optJSONObject(0) == null
|
||||
? "-"
|
||||
: versions.optJSONObject(0).optString("createdAt", "-");
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"主 Agent 复核记录",
|
||||
"最近一次复核 " + reviewTime + " · 对比线程提交摘要、测试结果和补丁说明后发布。",
|
||||
""
|
||||
));
|
||||
setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ConversationInfoActivityTest {
|
||||
@Test
|
||||
public void renderConversationUsesLightweightHeaderMenuAndThreadList() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestConversationInfoActivity activity = Robolectric
|
||||
.buildActivity(TestConversationInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderConversation",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "单线程会话"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "发起群聊"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程加入新群"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "线程详情"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(2), "查看当前线程聊天与项目"));
|
||||
assertTrue(viewTreeContainsText(content, "参与线程"));
|
||||
assertTrue(viewTreeContainsText(content, "硬件审计协作"));
|
||||
assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊"));
|
||||
assertFalse(viewTreeContainsText(content, "以下线程参与当前会话,点击可查看对应项目详情。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threadDetailMenuRowStillOpensProjectDetail() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ConversationInfoActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestConversationInfoActivity activity = Robolectric
|
||||
.buildActivity(TestConversationInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderConversation",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
);
|
||||
|
||||
View threadDetailRow = findClickableViewContainingText(
|
||||
activity.findViewById(R.id.screen_content),
|
||||
"线程详情"
|
||||
);
|
||||
assertNotNull(threadDetailRow);
|
||||
|
||||
threadDetailRow.performClick();
|
||||
|
||||
Intent nextIntent = Shadows.shadowOf(activity).getNextStartedActivity();
|
||||
assertNotNull(nextIntent);
|
||||
assertEquals(
|
||||
ProjectDetailActivity.class.getName(),
|
||||
nextIntent.getComponent().getClassName()
|
||||
);
|
||||
assertEquals(
|
||||
"project-1",
|
||||
nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_ID)
|
||||
);
|
||||
assertEquals(
|
||||
"北区试产线回归",
|
||||
nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload() throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "thread-7")
|
||||
.put("folderName", "Boss");
|
||||
JSONObject project = new JSONObject()
|
||||
.put("id", "project-1")
|
||||
.put("name", "北区试产线回归")
|
||||
.put("isGroup", false)
|
||||
.put("deviceIds", new JSONArray().put("mac-studio").put("macbook"))
|
||||
.put("threadMeta", threadMeta);
|
||||
return new JSONObject().put("project", project);
|
||||
}
|
||||
|
||||
private static JSONObject buildParticipantsPayload() throws Exception {
|
||||
JSONArray participants = new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("projectId", "project-1")
|
||||
.put("threadDisplayName", "北区试产线回归")
|
||||
.put("folderName", "Boss")
|
||||
.put("deviceId", "Mac Studio")
|
||||
.put("threadId", "thread-7")
|
||||
.put("isSourceProject", true))
|
||||
.put(new JSONObject()
|
||||
.put("projectId", "project-2")
|
||||
.put("threadDisplayName", "硬件审计协作")
|
||||
.put("folderName", "Boss")
|
||||
.put("deviceId", "Mac Studio")
|
||||
.put("threadId", "thread-8"));
|
||||
return new JSONObject().put("participants", participants);
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (expectedText.contentEquals(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static View findClickableViewContainingText(View root, String expectedText) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
if (viewTreeContainsText(root, expectedText) && root.isClickable()) {
|
||||
return root;
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return null;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
View match = findClickableViewContainingText(group.getChildAt(index), expectedText);
|
||||
if (match != null) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class TestConversationInfoActivity extends ConversationInfoActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render the lightweight info state directly.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class GroupInfoActivityTest {
|
||||
@Test
|
||||
public void renderGroupUsesLightweightHeaderMenuAndMemberList() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderGroup",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "巡检协作群"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(0), "协作群聊"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "线程详情"));
|
||||
assertTrue(viewTreeContainsText(content.getChildAt(1), "查看当前群聊对应项目"));
|
||||
assertTrue(viewTreeContainsText(content, "群成员"));
|
||||
assertTrue(viewTreeContainsText(content, "Boss 移动控制台"));
|
||||
assertFalse(viewTreeContainsText(content, "群聊成员可点击查看对应项目详情。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threadDetailMenuRowStillOpensProjectDetail() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderGroup",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload())
|
||||
);
|
||||
|
||||
View threadDetailRow = findClickableViewContainingText(
|
||||
activity.findViewById(R.id.screen_content),
|
||||
"线程详情"
|
||||
);
|
||||
assertNotNull(threadDetailRow);
|
||||
|
||||
threadDetailRow.performClick();
|
||||
|
||||
Intent nextIntent = Shadows.shadowOf(activity).getNextStartedActivity();
|
||||
assertNotNull(nextIntent);
|
||||
assertEquals(
|
||||
ProjectDetailActivity.class.getName(),
|
||||
nextIntent.getComponent().getClassName()
|
||||
);
|
||||
assertEquals(
|
||||
"group-1",
|
||||
nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_ID)
|
||||
);
|
||||
assertEquals(
|
||||
"巡检协作群",
|
||||
nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload() throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "group-thread-3")
|
||||
.put("folderName", "Boss");
|
||||
JSONObject project = new JSONObject()
|
||||
.put("id", "group-1")
|
||||
.put("name", "巡检协作群")
|
||||
.put("isGroup", true)
|
||||
.put("collaborationMode", "development")
|
||||
.put("threadMeta", threadMeta);
|
||||
return new JSONObject().put("project", project);
|
||||
}
|
||||
|
||||
private static JSONObject buildParticipantsPayload() throws Exception {
|
||||
JSONArray participants = new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("projectId", "group-1")
|
||||
.put("threadDisplayName", "巡检协作群")
|
||||
.put("folderName", "Boss")
|
||||
.put("deviceId", "Mac Studio")
|
||||
.put("threadId", "group-thread-3")
|
||||
.put("isSourceProject", true))
|
||||
.put(new JSONObject()
|
||||
.put("projectId", "project-2")
|
||||
.put("threadDisplayName", "Boss 移动控制台")
|
||||
.put("folderName", "Boss")
|
||||
.put("deviceId", "MacBook Pro")
|
||||
.put("threadId", "thread-8"));
|
||||
return new JSONObject().put("participants", participants);
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (expectedText.contentEquals(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static View findClickableViewContainingText(View root, String expectedText) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
if (viewTreeContainsText(root, expectedText) && root.isClickable()) {
|
||||
return root;
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return null;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
View match = findClickableViewContainingText(group.getChildAt(index), expectedText);
|
||||
if (match != null) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class TestGroupInfoActivity extends GroupInfoActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render the lightweight info state directly.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
@@ -110,8 +111,83 @@ public class ProjectDetailActivityUiTest {
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, true)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(attachmentView, "让 AI 分析"));
|
||||
assertTrue(viewTreeContainsText(attachmentView, "待分析"));
|
||||
assertTrue(viewTreeContainsText(attachmentView, "AI 分析"));
|
||||
assertTrue(viewTreeContainsText(attachmentView, "可分析"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void manualAnalysisAttachmentUsesCompactStatusAndActionCopy() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject attachment = new JSONObject()
|
||||
.put("attachmentId", "att-compact")
|
||||
.put("fileName", "现场录屏.mp4")
|
||||
.put("mimeType", "video/mp4")
|
||||
.put("attachmentKind", "video")
|
||||
.put("analysisState", "ready_manual")
|
||||
.put("fileSizeBytes", 2048);
|
||||
JSONObject message = new JSONObject()
|
||||
.put("id", "msg-compact")
|
||||
.put("kind", "attachment")
|
||||
.put("body", "已发送附件")
|
||||
.put("attachments", new JSONArray().put(attachment));
|
||||
|
||||
View attachmentView = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildAttachmentMessageView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, message),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "你"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "09:26"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, true)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(attachmentView, "AI 分析"));
|
||||
assertTrue(viewTreeContainsText(attachmentView, "可分析"));
|
||||
assertFalse(viewTreeContainsText(attachmentView, "让 AI 分析"));
|
||||
assertFalse(viewTreeContainsText(attachmentView, "待分析"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedAnalysisAttachmentPrefersSummaryOverRepeatedState() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "北区试产线回归");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject attachment = new JSONObject()
|
||||
.put("attachmentId", "att-complete")
|
||||
.put("fileName", "巡检录像.mp4")
|
||||
.put("mimeType", "video/mp4")
|
||||
.put("attachmentKind", "video")
|
||||
.put("analysisState", "completed")
|
||||
.put("analysisSummary", "发现设备旁存在临时工具箱")
|
||||
.put("fileSizeBytes", 4096);
|
||||
JSONObject message = new JSONObject()
|
||||
.put("id", "msg-complete")
|
||||
.put("kind", "attachment")
|
||||
.put("body", "已发送附件")
|
||||
.put("attachments", new JSONArray().put(attachment));
|
||||
|
||||
View attachmentView = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildAttachmentMessageView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, message),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "你"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "09:26"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, true)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(attachmentView, "发现设备旁存在临时工具箱"));
|
||||
assertFalse(viewTreeContainsText(attachmentView, "已分析"));
|
||||
}
|
||||
|
||||
private static View buildBoundMessageView(TestProjectDetailActivity activity, String messageId, String body) {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectGoalsActivityUiTest {
|
||||
@Test
|
||||
public void renderGoalsUsesCompactWechatCards() throws Exception {
|
||||
TestProjectGoalsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectGoalsActivity.class, new Intent()
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectGoalsActivity.EXTRA_PROJECT_NAME, "北区试产线回归"))
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderGoals",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildProject())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "主 Agent 已整理项目目标 · 已完成 1/3"));
|
||||
assertTrue(viewTreeContainsSubstring(content, "完成北区试产线全链路回归"));
|
||||
assertTrue(viewTreeContainsSubstring(content, "已完成 · 09:12 由主 Agent 复核"));
|
||||
assertTrue(viewTreeContainsText(content, "当前约束"));
|
||||
assertFalse(viewTreeContainsText(content, "标记完成"));
|
||||
assertFalse(viewTreeContainsText(content, "编辑目标"));
|
||||
assertFalse(((SwipeRefreshLayout) activity.findViewById(R.id.screen_refresh_layout)).isRefreshing());
|
||||
}
|
||||
|
||||
private static JSONObject buildProject() throws Exception {
|
||||
JSONArray goals = new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("id", "goal-1")
|
||||
.put("text", "完成北区试产线全链路回归,覆盖串口、视觉、OTA 和容灾切换。")
|
||||
.put("note", "已完成 · 09:12 由主 Agent 复核")
|
||||
.put("state", "completed"))
|
||||
.put(new JSONObject()
|
||||
.put("id", "goal-2")
|
||||
.put("text", "所有关键步骤必须留下可交接证据,禁止仅口头确认。")
|
||||
.put("note", "进行中 · 允许用户编辑,主 Agent 会同步重排任务")
|
||||
.put("state", "pending"))
|
||||
.put(new JSONObject()
|
||||
.put("id", "goal-3")
|
||||
.put("text", "当线程上下文余量进入 urgent 前,必须完成阶段摘要与 handoff。")
|
||||
.put("note", "待处理 · 主 Agent 会优先把压缩前必须收尾的任务推到前面")
|
||||
.put("state", "pending"));
|
||||
return new JSONObject().put("goals", goals);
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (expectedText.contentEquals(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsSubstring(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (text != null && text.toString().contains(expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsSubstring(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class TestProjectGoalsActivity extends ProjectGoalsActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests drive renderGoals manually.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectVersionsActivityUiTest {
|
||||
@Test
|
||||
public void renderVersionsUsesReadonlyWechatStyleCards() throws Exception {
|
||||
TestProjectVersionsActivity activity = Robolectric
|
||||
.buildActivity(TestProjectVersionsActivity.class, new Intent()
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_ID, "project-1")
|
||||
.putExtra(ProjectVersionsActivity.EXTRA_PROJECT_NAME, "北区试产线回归"))
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderVersions",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildProject())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "仅主 Agent 可发布迭代记录"));
|
||||
assertTrue(viewTreeContainsText(content, "v1.2.8 已发布"));
|
||||
assertTrue(viewTreeContainsSubstring(content, "• 优化 OTA 实时提示"));
|
||||
assertTrue(viewTreeContainsText(content, "主 Agent 复核记录"));
|
||||
assertFalse(viewTreeContainsText(content, "版本记录只读"));
|
||||
assertFalse(((SwipeRefreshLayout) activity.findViewById(R.id.screen_refresh_layout)).isRefreshing());
|
||||
}
|
||||
|
||||
private static JSONObject buildProject() throws Exception {
|
||||
JSONArray versions = new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("version", "v1.2.8 已发布")
|
||||
.put("summary", "• 优化 OTA 实时提示\n• 修复主 Agent 版本汇总延迟\n• 补齐北区试产线回归记录")
|
||||
.put("createdAt", "09:40"))
|
||||
.put(new JSONObject()
|
||||
.put("version", "v1.2.7 已归档")
|
||||
.put("summary", "• 接入设备页状态色\n• 新增账号二维码入口\n• 优化会话页排序")
|
||||
.put("createdAt", "昨日"));
|
||||
return new JSONObject().put("versions", versions);
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (expectedText.contentEquals(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsSubstring(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (text != null && text.toString().contains(expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsSubstring(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class TestProjectVersionsActivity extends ProjectVersionsActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests drive renderVersions manually.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user