# WeChat Native UI Phase 2 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Polish the native Android WeChat-style rollback with better chat feel, clearer OTA download/install feedback, and smoother root navigation memory. **Architecture:** Keep the approved WeChat-style root surfaces intact and limit this batch to interaction polish. Add small pure-Java helpers for chat UI state and OTA download state so the tricky behavior is unit-tested, then wire those helpers into `ProjectDetailActivity`, `AboutActivity`, and `MainActivity`. **Tech Stack:** Android AppCompat, XML layouts, Java 21, Gradle 8, JUnit4, DownloadManager, existing Boss APIs, existing release packaging and deploy scripts. --- ## File Structure ### New files - `android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java` - Pure-Java helper for send-button enabled state, optimistic pending bubble state, and auto-scroll policy. - `android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java` - Pure-Java helper for mapping `DownloadManager` progress/status into UI strings and retry/install states. - `android/app/src/main/java/com/hyzq/boss/RootTabMemory.java` - Pure-Java helper for resolving explicit root tabs versus stored root tabs. - `android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java` - Unit tests for chat send/pending/scroll rules. - `android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java` - Unit tests for OTA download state mapping. - `android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java` - Unit tests for remembered root tab selection. ### Modified files - `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java` - `android/app/src/main/java/com/hyzq/boss/AboutActivity.java` - `android/app/src/main/java/com/hyzq/boss/MainActivity.java` - `android/app/src/main/java/com/hyzq/boss/BossUi.java` - `README.md` - `docs/architecture/current_runtime_and_deploy_status_cn.md` - `docs/architecture/api_and_service_inventory_cn.md` ## Task 1: Polish Chat Composer Feedback and Scroll Behavior **Files:** - Create: `android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java` - Create: `android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java` - Modify: `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java` - Modify: `android/app/src/main/java/com/hyzq/boss/BossUi.java` - [ ] **Step 1: Write the failing test** ```java package com.hyzq.boss; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; public class ProjectChatUiStateTest { @Test public void sendEnabled_requiresTextAndNotBusy() { assertFalse(ProjectChatUiState.canSend("", false)); assertFalse(ProjectChatUiState.canSend(" ", false)); assertFalse(ProjectChatUiState.canSend("你好", true)); assertTrue(ProjectChatUiState.canSend("你好", false)); } @Test public void shouldAutoScroll_onlyWhenNearBottomOrForced() { assertTrue(ProjectChatUiState.shouldAutoScroll(true, false)); assertTrue(ProjectChatUiState.shouldAutoScroll(false, true)); assertFalse(ProjectChatUiState.shouldAutoScroll(false, false)); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon ``` Expected: FAIL with missing `ProjectChatUiState`. - [ ] **Step 3: Write minimal implementation** ```java package com.hyzq.boss; public final class ProjectChatUiState { private ProjectChatUiState() {} public static boolean canSend(String text, boolean sending) { return !sending && text != null && !text.trim().isEmpty(); } public static boolean shouldAutoScroll(boolean nearBottom, boolean forced) { return nearBottom || forced; } } ``` Then integrate it into `ProjectDetailActivity`: - disable send button when text empty or request busy - show a local pending outgoing bubble immediately after tapping send - only auto-scroll on refresh when user was already near bottom or after local send - [ ] **Step 4: Run tests and compile verification** Run: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android :app:compileDebugJavaWithJavac --no-daemon ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash cd /Users/kris/code/boss git add android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java \ android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java \ android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java \ android/app/src/main/java/com/hyzq/boss/BossUi.java git commit -m "feat: polish native chat composer feedback" ``` ## Task 2: Add OTA Download Progress and Retry Guidance **Files:** - Create: `android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java` - Create: `android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java` - Modify: `android/app/src/main/java/com/hyzq/boss/AboutActivity.java` - Modify: `android/app/src/main/java/com/hyzq/boss/BossUi.java` - [ ] **Step 1: Write the failing test** ```java package com.hyzq.boss; import static org.junit.Assert.assertEquals; import org.junit.Test; public class OtaDownloadStateMapperTest { @Test public void toProgressLabel_formatsKnownProgress() { assertEquals("已下载 50%", OtaDownloadStateMapper.toProgressLabel(50, true)); } @Test public void toProgressLabel_handlesUnknownProgress() { assertEquals("正在准备下载", OtaDownloadStateMapper.toProgressLabel(0, false)); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.OtaDownloadStateMapperTest --no-daemon ``` Expected: FAIL with missing `OtaDownloadStateMapper`. - [ ] **Step 3: Write minimal implementation** ```java package com.hyzq.boss; public final class OtaDownloadStateMapper { private OtaDownloadStateMapper() {} public static String toProgressLabel(int percent, boolean hasKnownTotal) { if (!hasKnownTotal) { return "正在准备下载"; } return "已下载 " + Math.max(0, Math.min(100, percent)) + "%"; } } ``` Then integrate it into `AboutActivity`: - query `DownloadManager` while download is active - render a visible progress row in page content - keep a retry action after failed download - show install permission guidance row when unknown-source install permission is missing - [ ] **Step 4: Run tests and compile verification** Run: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.OtaDownloadStateMapperTest --no-daemon JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android :app:compileDebugJavaWithJavac --no-daemon ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash cd /Users/kris/code/boss git add android/app/src/main/java/com/hyzq/boss/OtaDownloadStateMapper.java \ android/app/src/test/java/com/hyzq/boss/OtaDownloadStateMapperTest.java \ android/app/src/main/java/com/hyzq/boss/AboutActivity.java \ android/app/src/main/java/com/hyzq/boss/BossUi.java git commit -m "feat: add native ota progress feedback" ``` ## Task 3: Remember Root Tab and Smooth Root Back Behavior **Files:** - Create: `android/app/src/main/java/com/hyzq/boss/RootTabMemory.java` - Create: `android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java` - Modify: `android/app/src/main/java/com/hyzq/boss/MainActivity.java` - [ ] **Step 1: Write the failing test** ```java package com.hyzq.boss; import static org.junit.Assert.assertEquals; import org.junit.Test; public class RootTabMemoryTest { @Test public void resolveInitialTab_prefersExplicitTab() { assertEquals("devices", RootTabMemory.resolveInitialTab("devices", "me")); } @Test public void resolveInitialTab_fallsBackToStoredTab() { assertEquals("me", RootTabMemory.resolveInitialTab(null, "me")); } @Test public void resolveInitialTab_defaultsToConversations() { assertEquals("conversations", RootTabMemory.resolveInitialTab(null, null)); } } ``` - [ ] **Step 2: Run test to verify it fails** Run the new unit test target after creating it in the same package: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --tests com.hyzq.boss.RootTabMemoryTest --no-daemon ``` Expected: FAIL with missing helper. - [ ] **Step 3: Write minimal implementation** Implement the helper: ```java package com.hyzq.boss; public final class RootTabMemory { private RootTabMemory() {} public static String resolveInitialTab(String explicitTab, String storedTab) { if ("conversations".equals(explicitTab) || "devices".equals(explicitTab) || "me".equals(explicitTab)) { return explicitTab; } if ("conversations".equals(storedTab) || "devices".equals(storedTab) || "me".equals(storedTab)) { return storedTab; } return "conversations"; } } ``` Then wire it into `MainActivity` so that: - last selected root tab is persisted in `SharedPreferences` - explicit deep-link tab still wins over stored tab - when already at root conversations tab, back key shows a soft toast then moves task to back - [ ] **Step 4: Run tests and full debug verification** Run: ```bash cd /Users/kris/code/boss JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android testDebugUnitTest --no-daemon JAVA_HOME=$(/usr/libexec/java_home) ./android/gradlew -p ./android assembleDebug --no-daemon ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash cd /Users/kris/code/boss git add android/app/src/main/java/com/hyzq/boss/MainActivity.java \ android/app/src/main/java/com/hyzq/boss/RootTabMemory.java \ android/app/src/test/java/com/hyzq/boss/RootTabMemoryTest.java git commit -m "feat: polish native root tab memory" ``` ## Task 4: Verification, Packaging, Deploy, and Docs **Files:** - Modify: `README.md` - Modify: `docs/architecture/current_runtime_and_deploy_status_cn.md` - Modify: `docs/architecture/api_and_service_inventory_cn.md` - Modify: `android/app/build.gradle` - [ ] **Step 1: Update docs and version** - bump Android version for the next polish release - document chat send feedback, OTA progress, and root tab memory behavior - [ ] **Step 2: Run local verification** Run: ```bash cd /Users/kris/code/boss npm run lint npm run build curl -sS http://127.0.0.1:3000/api/health curl -sS http://127.0.0.1:4317/health JAVA_HOME=$(/usr/libexec/java_home) npm run apk:release JAVA_HOME=$(/usr/libexec/java_home) npm run aab:release ``` - [ ] **Step 3: Deploy and verify** Run: ```bash cd /Users/kris/code/boss BOSS_SERVER_PASS='your-password' ./scripts/deploy-server.sh "$HOME/.codex/skills/boss-server-debug/scripts/server_ssh.sh" exec "curl -sS http://127.0.0.1:3000/api/health" curl -sS https://boss.hyzq.net/api/health curl -sS https://boss.hyzq.net/downloads/boss-android-latest.json ``` - [ ] **Step 4: Commit** ```bash cd /Users/kris/code/boss git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md \ docs/architecture/api_and_service_inventory_cn.md android/app/build.gradle \ public/downloads/ git commit -m "chore: publish native ui phase 2 polish release" ```