# P7:权限快捷引导 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax. **不执行任何 git 操作**(用户要求,改动留工作区)。不创建额外任务清单。 **Goal:** 引导页权限步骤从纯文字改可操作状态清单;automation 页补齐通知监听/短信/存储三项权限的状态检测与快速跳转按钮。不新增原生权限,只加 UI 入口和一座通知监听状态检测 bridge 方法。 --- ## 背景决策 - **不新增权限**。所有权限已在 AndroidManifest 中声明(通过 Config Plugin),P7 只加 UI 入口。 - **通知监听状态检测**需要一个原生 bridge 方法(`isNotificationListenerEnabled`),因为 RN 侧无法直接检查。用 `NotificationManager.getEnabledListenerPackages()`。 - **短信 + 存储**用 React Native 内置 `PermissionsAndroid` API。 - **引导页 UI** 沿用现有 Card + Button 组件,与 P1-P5 设计系统一致。 --- ### Task 1: 新增桥接方法 `isNotificationListenerEnabled` **Files:** - Modify: `plugins/accessibility/android/AccessibilityBridgeModule.kt` - Modify: `src/services/accessibilityBridge.ts` - [ ] **Step 1: AccessibilityBridgeModule.kt 加方法** ```kotlin @ReactMethod fun isNotificationListenerEnabled(promise: Promise) { try { val pm = reactContext.packageManager val enabledListeners = android.app.NotificationManager::class.java .getMethod("getEnabledListenerPackages") .invoke(reactContext.getSystemService(Context.NOTIFICATION_SERVICE)) as? List val myPkg = reactContext.packageName promise.resolve(enabledListeners?.any { it.startsWith(myPkg) } == true) } catch (e: Exception) { promise.resolve(false) } } ``` **注意**:`getEnabledListenerPackages()` 在 Android 11+ 需 `isNotificationListenerEnabled(ComponentName)`;优先用反射避免 API level lint 错误,反射失败返回 false。 - [ ] **Step 2: JS 侧 bridge 接口** `NativeAccessibilityBridge` 加 `isNotificationListenerEnabled(): Promise;` - [ ] **Step 3: 编译验证** `cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL --- ### Task 2: 引导页权限步骤(`_onboarding.tsx` 重写 permissions 步骤) **Files:** - Modify: `src/app/_onboarding.tsx` - Modify: `src/i18n/zh.ts`、`src/i18n/en.ts`(少量新键) - [ ] **Step 1: i18n 新键(zh/en 对等)** 在 `onboarding` section 加: ``` permAccessibility 无障碍服务 / Accessibility Service permNotification 通知监听 / Notification Listener permOverlay 悬浮窗 / Overlay permSms 短信读取 / SMS permStorage 相册/存储 / Photos & Storage permGranted 已授权 / Granted permNotGranted 未授权 / Not Granted permOpenSettings 前往设置 / Open Settings permRequest 请求权限 / Request ``` - [ ] **Step 2: 重写 permissions 步骤** 替换 `_onboarding.tsx:48` 的 `key: 'permissions'` 步骤内容。在当前 description 下方加权限状态清单(Card 包裹,每行:图标 + 名称 + 状态点 + 操作按钮)。 权限列表: 1. **无障碍服务**(`BIND_ACCESSIBILITY_SERVICE`)—— 最核心。检测 `serviceRunning`(从 `getAccessibilityBridge().isServiceRunning()` 异步取)。未授权 → 跳转 `ACCESSIBILITY_SETTINGS` 2. **通知监听**(`BIND_NOTIFICATION_LISTENER_SERVICE`)—— 检测 `bridge.isNotificationListenerEnabled()`。未授权 → 跳转 `ACTION_NOTIFICATION_LISTENER_SETTINGS` 3. **短信**(`RECEIVE_SMS`)—— `PermissionsAndroid.check()`。未授权 → `PermissionsAndroid.request()` 4. **存储**(`READ_MEDIA_IMAGES`,Android 13+ 用 `READ_MEDIA_IMAGES`,否则 `READ_EXTERNAL_STORAGE`)—— 同上 **实现细节**: - `const [permStates, setPermStates] = useState>({})` —— null = 加载中 - `useEffect(() => { checkAllPermissions(); }, [])` —— 步骤切到 permissions 时触发 - `checkAllPermissions`:调 `PermissionsAndroid.check()` 检查短信和存储;调 bridge 检查无障碍和通知监听 - 每行渲染:`Ionicons` 图标 + `permLabel` + 右侧状态文字(绿「已授权」/ 灰「未授权」)+ 未授权时显示操作按钮 - 「全部已授权」时显示一个大绿色勾 + 提示文字,按钮不显示 **权限 API 版本适配**: - `PermissionsAndroid.PERMISSIONS` 里 Android 13+ 是 `READ_MEDIA_IMAGES`,低版本用 `READ_EXTERNAL_STORAGE` - 统一用 `Platform.Version >= 33 ? 'android.permission.READ_MEDIA_IMAGES' : 'android.permission.READ_EXTERNAL_STORAGE'` - [ ] **Step 3: 验证** Run: `npm run typecheck && npm test` → 全绿 Run: `grep -rn "onboarding.perm" src/i18n/zh.ts src/i18n/en.ts` → 确认键对等 --- ### Task 3: automation 页补齐权限入口 **Files:** - Modify: `src/app/automation/index.tsx` - Modify: `src/i18n/zh.ts`、`src/i18n/en.ts`(少量新键) - [ ] **Step 1: i18n 新键** 在 `automation` section 加: ``` notificationTitle 通知监听服务 / Notification Listener notificationEnabled 已启用 / Enabled notificationDisabled 未启用 / Disabled notificationOpenSettings 前往系统设置 / Open Settings smsPermissionTitle 短信读取权限 / SMS Permission smsPermissionGranted 已授权 / Granted smsPermissionRequest 请求权限 / Request storagePermissionTitle 存储权限 / Storage Permission storagePermissionGranted 已授权 / Granted storagePermissionRequest 请求权限 / Request ``` - [ ] **Step 2: automation 页加权限卡片** 在现有「无障碍服务状态与控制」Card **之后**加一个新 Card:``。 内容三行: 1. **通知监听**:异步 `bridge.isNotificationListenerEnabled()` → 状态文字 + 未启用时「前往系统设置」按钮(`Linking.sendIntent('android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS')`) 2. **短信**:`PermissionsAndroid.check('android.permission.RECEIVE_SMS')` + `PermissionsAndroid.request()` 3. **存储**:`PermissionsAndroid.check(StoragePermission)` + `PermissionsAndroid.request()` **实现模式**: ```tsx const [notifEnabled, setNotifEnabled] = useState(false); const [smsGranted, setSmsGranted] = useState(false); const [storageGranted, setStorageGranted] = useState(false); useEffect(() => { // 异步获取各权限状态 checkNotif(); checkSms(); checkStorage(); }, []); ``` 每行渲染:`Ionicons` 图标 + 权限名称 + 状态文字 + 未授权时的按钮。 - [ ] **Step 3: 验证** Run: `npm run typecheck && npm test` → 全绿 --- ### Task 4: 编译 + 测试 + 终审 - [ ] **Step 1: 编译验证** `cd android && ./gradlew :app:compileDebugKotlin` → BUILD SUCCESSFUL - [ ] **Step 2: 全量测试** `npm test` → 全绿;`npm run typecheck` → 0 错误 - [ ] **Step 3: 审计** ```bash grep -rn "#[0-9A-Fa-f]\{6\}" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts" # 无输出 grep -rn "fontFamily" src/app/_onboarding.tsx src/app/automation/index.tsx # 无输出 grep -rnE "📊|💰|💸|📝|🔄|🎉|✨|⚠|📌|🌟" src/i18n/ src/app/_onboarding.tsx src/app/automation/index.tsx # 无输出(预存 i18n fallbackWarn ⚠ 除外) ``` - [ ] **Step 4: 手工走查(需设备)** 1. 清数据 → 启动应用 → 引导页第 5 步(权限)→ 看到 4 个权限状态 2. 点未授权的无障碍 → 跳系统无障碍设置 → 返回看到状态已变 3. 点未授权的通知监听 → 跳系统通知设置 4. 点短信 → 弹出系统权限对话框 5. 引导页全部完成后 → 进入 automation 页 → 看到新增的权限卡片 --- ## 终审修订(已实施,以实际代码为准) **终审日期**:2026-07-22,审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。 | # | 级别 | 问题 | 处理 | |---|---|---|---| | — | — | 无 Critical / Important 发现 | — | **偏差说明**: 1. `onboarding.permOverlay`(悬浮窗)未加入引导页——计划 Task 2 决策列为可选项,实际未实现。不影响核心功能(sideload 自动授予 + 无障碍运行时浮窗走 ACCESSIBILITY_OVERLAY 不依赖此权限)。 2. T3 中 `Platform.Version >= 33` 改为 `Number(Platform.Version) >= 33`——React Native 的 `Platform.Version` 类型为 `string | number`,直接比较会 TS 报错。 3. T1 的 `isNotificationListenerEnabled` 使用反射而非 `enabledListenerPackages` 属性——minSdk=24 < API 27,反射是兼容方案。