Android 端首次启动 onboarding 服务,把用户 walk through 系统电池不限 + 各 OEM 自家的后台 / 自启动设置页。落地于 727ff55(TAPD 1003034 / 1003057 收口),随之配合 01-平台集成 §前台服务 的 PI-scan / wake-lock / MAC-filter callout 链——后者是 OS 级 scan-callback 路径,本服务负责 OS 级路径之上的"用户 OEM 权限"那一层。iOS 不走本服务(BG BLE 由 iOS State Preservation & Restoration 单独负责)。
lib/core/services/background_permissions_service.dart —— Dart 侧 singleton BackgroundPermissionsService.instance + OemHint enum,包 culinatech.app/background_permissions MethodChannel 的六个调用android/app/src/main/kotlin/com/example/culinatech_app/BackgroundPermissionsHelper.kt —— Kotlin object,封装 PowerManager.isIgnoringBatteryOptimizations + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 系统 Intent + 各 OEM Activity ComponentName 候选表 + Intent.resolveActivity best-effort dispatchandroid/app/src/main/kotlin/com/example/culinatech_app/MainActivity.kt —— 在 configureFlutterEngine 里建 BG_PERMISSIONS_CHANNEL = "culinatech.app/background_permissions" MethodChannel,把五个方法调用直接转发到 BackgroundPermissionsHelperlib/main.dart —— _AppInitializerState._maybeShowBgPermsDialog(首次启动 onboarding 触发器;字段名 _bgPermsDialogShown 与 prefs key bg_perms_dialog_seen_v1 历史沿用 dialog 命名,仍准确)。2dfb46b 起从 showDialog(_BgPermsDialog) 改为 nav.push(MaterialPageRoute<void>(name='/background-permission', builder: BackgroundPermissionPage))lib/features/onboarding/background_permission_page.dart —— BackgroundPermissionPage(StatefulWidget + WidgetsBindingObserver,full-screen Scaffold;2dfb46b 引入,TAPD 1003054)+ 内部 _BgStepRow(单步 UI cell,等价旧 _StepRow)+ _BgStepDivider(cell 间分隔);与 permissions_intro_page.dart 同 lib/features/onboarding/ 目录、视觉风格对齐Per Liang 2026-05-22(727ff55):BG reconnect 在 MIUI / VIVO / OPPO 上的秒级 ceiling 是 OS-imposed——Liang 在 PI-scan APK 上跑 1003034 / 自启动 OFF(log culinatech_log_20260522152046.txt)实测 OS DOES 在 BG / 锁屏 / 灭屏下投递 wakeful broadcasts,但 MIUI 的 Doze maintenance window 把 delivery batch 10–40 分钟(vs 自启动 ON 的 ~6 分钟)。Liang 接受这是 OS 上限并下「豁免」verdict("一般用户应该不会去开,它藏的比较深"),要求 App 在首次启动时把用户 walk through 系统电池不限 + 各 OEM 自启动设置,因为 stock Android 的 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS Intent 不触达 OEM 上层策略过滤器。
BackgroundPermissionsService.instance 是私构造的 singleton;仅 kIsWeb 会直接 short-circuit,其他非 Android 平台则依赖 MissingPluginException / 默认返回值兜底。MissingPluginException 与一般异常都会返回安全默认(false / OemHint.unknown);但并非所有一般异常都会记 BG_PERMS_* 日志,部分方法仅静默 fallback。
| 方法 | 返回 | 行为 |
|---|---|---|
getOemHint() |
Future<OemHint> |
读 Build.MANUFACTURER 粗分桶。Honor/Redmi/Poco/iQOO/Realme/OnePlus 折叠到 parent brand(同 restriction filter) |
isBatteryUnrestricted() |
Future<bool> |
PowerManager.isIgnoringBatteryOptimizations(packageName) |
requestBatteryUnrestricted() |
Future<bool> |
发 Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 系统 dialog,Uri.parse("package:<pkg>") |
openBatterySettings() |
Future<bool> |
三段路径:(a) Xiaomi/MIUI/HyperOS → com.miui.powerkeeper / HiddenAppsConfigActivity 直达本 App 的 省电策略(无限制 / 智能限制 / 限制后台活动)chooser,package_name + package_label extras 一并塞;(b) 非 Xiaomi(Samsung / Pixel / OPPO / vivo / Huawei …,001711f / TAPD #1003061,Liang OK 2026-05-27)→ 调 requestBatteryUnrestricted() 发 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 系统 dialog,用户点 Allow 一步把 isIgnoringBatteryOptimizations 翻 true;(c) fallback → Settings.ACTION_APPLICATION_DETAILS_SETTINGS(App-info 页,必 resolve)。故意不再用 ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS 全 App 列表页——Liang 之前明确标错(用户得在长列表里找到自己 App) |
openOemAutostart() |
Future<bool> |
按 OEM 走候选 ComponentName 列表,第一个 resolveActivity 命中即 startActivity;fallback 到 battery settings,再 fallback 到 Settings.ACTION_APPLICATION_DETAILS_SETTINGS(app-info 页,必然 resolve) |
OemHint enum:pixelOrStock | xiaomi | huawei | oppo | vivo | samsung | other | unknown。hasOemAutostart getter 在 xiaomi / huawei / oppo / vivo 时返 true——UI 据此决定是否渲染第二个「打开 OEM 自启动设置」按钮。Samsung 走 false(设备护理深得不可靠,falls through to 系统电池设置)。
BackgroundPermissionsHelper.candidatesForOem(Oem) 返回的 ComponentName 列表(顺序 = 试探优先级)。每个 candidate 都过 Intent.resolveActivity 守卫,缺失版本静默 skip 下一条;公开逆向工程出处见 Kotlin 源文件 doc-comment。
| OEM | 优先级最高的 Activity(package / class) |
|---|---|
| MIUI / Xiaomi | com.miui.securitycenter / com.miui.permcenter.autostart.AutoStartManagementActivity |
| EMUI / Huawei | com.huawei.systemmanager / com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity |
| ColorOS / OPPO | com.coloros.safecenter / com.coloros.safecenter.permission.startup.StartupAppListActivity |
| FuntouchOS / VIVO | com.iqoo.secure / com.iqoo.secure.ui.phoneoptimize.BgStartUpManager |
| Samsung | (空,落到 battery-settings fallback) |
| Pixel / stock / other | (空,落到 battery-settings fallback) |
MIUI 还有一条 older com.miui.permcenter.MainAcitivty 备选;EMUI 有 ProtectActivity + StartupAppControlActivity 两条 older fallback;ColorOS 有 3 条 older / OPPO 品牌期 fallback;FuntouchOS 有 AddWhiteListActivity + vivo.permissionmanager.BgStartUpManagerActivity 两条。完整名单看源文件。
main.dart:_AppInitializerState._maybeShowBgPermsDialog 在 postFrameCallback 里串行跑,在 _maybeShowMissingPermissionsDialog + _maybeShowLowVolumeDialog 之后、homeOnboardingProvider.notifier.maybeShow()(首次启动 dashboard coach-marks,TAPD #1003076 / 45d9916,详见 01-平台集成 §权限请求策略 §First-launch 引导链尾段)之前。Skip 闸(任一命中即跳过):
kIsWeb / iOS)_bgPermsDialogShown == true(防 locale-rebuild 重弹)bg_perms_dialog_seen_v1== true(持久化 seen 标,跨进程重启不再弹)isBatteryUnrestricted() == true(用户已经开过,无需再 nag——同时顺手把 bg_perms_dialog_seen_v1 置 true 防之后偶尔关掉再触发)通过这四道闸后 _bgPermsDialogShown = true、oem = await svc.getOemHint(),nav.push(MaterialPageRoute<void>(name: '/background-permission', builder: BackgroundPermissionPage(oem: oem)))(2dfb46b 起;之前是 showDialog(_BgPermsDialog)),页面 pop 后写持久化 bg_perms_dialog_seen_v1 = true——跳过 与 完成 两个按钮都走同一 Navigator.pop(context) 路径、都置 flag(一旦用户见过这个 onboarding 永不复弹,无论是否实际开启了设置)。
⚠️ 1fe6b3e3 / TAPD #1003291 起加
BackgroundKillWatchdog给「skip 了 / 关了自启动」的用户第二次机会:首次 onboarding 可跳且自启动 step 无 OS 反馈 API,用户跳过或只开电池没开自启动都没有再 nag 的机会;#1003291 实证 Xiaomi BLE 模式 14 h 中 8.5 h log 全静默(FG service + wake lock + START_STICKY 都拦不住 MIUI overnight reclaim),冷启动 5 s 重连恢复(不是 BLE bug)。新BackgroundKillWatchdog(lib/core/services/background_kill_watchdog.dart,单例 + 单元测试 9 个)记录「bg 期是否在监测」+「bg 起始时戳」到 SharedPreferences keybg_watchdog_monitoring_v1/bg_watchdog_since_ms_v1,由_AppExitObserver._setBackgroundedAndroid 路径喂——backgrounded时若connectedBoostersProvider非空则置monitoring=true、resumed时清,正常 bg→fg 轮转不经main()、monitoring留 true 才说明进程被回收过。_AppInitializer.initState第一件事captureColdStartVerdict()拉前生留下的 flag、返回BgKillVerdict { wasMonitoring, outage },_AppInitializerpost-frame 链尾在_maybeShowBgPermsDialog之后串新一条_maybeReNudgeAfterKill:当nagWorthy = wasMonitoring && outage ≥ killGapThreshold (10 min)且oem.hasOemAutostart == true(仅 OEM 自启动 filter 存在的机型——Xiaomi / Huawei / OPPO / vivo;其它 OEM 即便 kill 也无可纠正设置、nag 是噪声)、且reNudgeAllowedNow()通过 3 天 cooldown(keybg_perms_renudge_last_ms_v1,单元测试 2 个 cooldown case + 7 个 evaluate case)时,再 push 一次同一BackgroundPermissionPage(oem: oem)走name: '/background-permission-renudge'路由,关 dialog 后markReNudgeShown()重置 cooldown。Time-source /now注入用于evaluate/reNudgeAllowed纯函数 unit test;wall-clock 倒拨防御:outage.isNegative时夹到Duration.zero防 tz / NTP correction 让超长 outage 伪装成 sub-threshold。_killVerdictFuture在initState即开始 await——latch 一次性、任何后续 foreground-resume 也不会先于此 clear flag。iOS / web 整条 skip(iOS CoreBluetooth restoration 保链路、re-nudge gated 在 Android)。新 DevLog tag:BG_PERMS_RENUDGE_SHOW(弹出前记oem+outage分钟数)、BG_PERMS_RENUDGE_SKIP(cooldown 内命中)、BG_PERMS_RENUDGE_DISMISSED(页面 pop)。
BackgroundPermissionPage)StatefulWidget + WidgetsBindingObserver:在 AppLifecycleState.resumed 时(用户从 Settings 回来)重读 isBatteryUnrestricted() 刷新 step 1 状态显示。_BgStepRow 单步 cell:圆形序号 + 标题 + 状态行(绿 ✓ / 黄警告 / 中性灰)+ 左对齐「打开 …」按钮;_BgStepDivider 在 cell 间画 0.5 px 6% 白色分隔线。
l.bgPermsBatteryTitle)。按钮 l.bgPermsBatteryBtnOpen / l.bgPermsBatteryBtnView → openBatterySettings();状态用 OS-readable isBatteryUnrestricted() 显示 l.bgPermsBatteryOn / l.bgPermsBatteryOff,用户从 Settings 回来时 lifecycle observer 触发 _refreshState()、绿 ✓ 自动闪现oem.hasOemAutostart 为 true):OEM 自启动 / 后台管理(l.bgPermsOemTitle ICU 占位 {oem})。按钮 label 由 l.bgPermsOemBtn(oemLabel) 拼出,oemLabel 取 l.bgPermsOemXiaomi / Huawei / Oppo / Vivo / Samsung(en 形式分别为 "Xiaomi / MIUI"、"Huawei / EMUI"、"OPPO / ColorOS"、"vivo / iQOO"、"Samsung",其它语言由 ARB 自然翻译) → openOemAutostart();状态行永远中性灰——OEM 自启动页没有可读回的 API,App 只能"信用户做过了"l.bgPermsSkip / l.bgPermsDone(两者行为相同:都 Navigator.pop())UI 主题:full-screen Scaffold 用 CulinaColors.background 底 + 白字 + step cell 用 CulinaColors.surface 背景 + 16px 圆角(带 6% 白色边框),顶部居中 56×56 px 主题色圆形 + battery 图标头部。布局走 scroll-with-pinned-footer idiom——LayoutBuilder → SingleChildScrollView → ConstrainedBox(minHeight: viewportHeight) → IntrinsicHeight → Column { Header + 内容 + Spacer + Footer }:内容短时 Spacer 吃掉空白把 footer 钉到底,内容长时 footer 随内容一起 scroll、不被裁。
🟡 v1 dialog → v2 full-screen page(2dfb46b,TAPD 1003054):本服务 ship 时是
AlertDialog(_BgPermsDialoginmain.dart),但 2-step OEM 场景或长 locale(de/es/fr/it)下 dialog 内部SingleChildScrollView与底部 actions 行抢空间,第二步的「打开 OEM 自启动设置」按钮会被底部 footer 切掉一半(TAPD 1003054 报告)。2dfb46b 整体迁到 full-screenBackgroundPermissionPage(新文件lib/features/onboarding/background_permission_page.dart,与PermissionsIntroPage同目录、视觉对齐),用 scroll-with-pinned-footer 彻底消除 clipping;同 commit 从main.dart删除_BgPermsDialog/_BgPermsDialogState/_StepRow三个 private class。bgPerms*ARB key(7969fc3 引入的 6 语本地化)100% 复用、未新增 string。
🟡 v1 Chinese-only → 6-locale 本地化(7969fc3):本服务 ship 时 UI 全部 hardcoded 中文("后台运行权限" / "跳过" / "完成" / "打开电池设置" / "打开 小米/MIUI 自启动设置" 等),en/de/es/fr/it 用户看到中文。7969fc3 把 14 个 string + 5 个 OEM brand label 搬到 ARB(
bgPerms*命名族),6 语完整本地化;OEM 品牌名(Xiaomi / Huawei / OPPO / vivo / Samsung)因品牌识别度故各语言保留原英文 / 拉丁拼写。
BG_PERMS_ONBOARDING_SKIP:skip 闸命中(持久化 flag 已设 / 电池已不受限 / 非 Android)BG_PERMS_ONBOARDING_SHOW:弹出前 log oem=<name> hasOemAutostart=<bool>BG_PERMS_ONBOARDING_DISMISSED:onboarding 页面 pop 后写持久化 flag(2dfb46b 起从 dialog 改 page,log tag 名沿用)BG_PERMS_REQUEST_BATTERY / BG_PERMS_REQUEST_BATTERY_RESULT / BG_PERMS_REQUEST_BATTERY_FAIL:系统电池 unrestricted dialog 路径BG_PERMS_OPEN_BATTERY_SETTINGS / BG_PERMS_OPEN_AUTOSTART:跳设置页时落 logBG_PERMS_OEM_HINT_FAIL:Channel 调用异常时 fallback 到 OemHint.unknown 的诊断 logIntent.resolveActivity 守卫确保缺失版本不 throw ActivityNotFoundException;fallback 链最终一定落到 ACTION_APPLICATION_DETAILS_SETTINGS(app-info 页,所有真实设备都 resolve)bg_perms_dialog_seen_v1 写入后永不复弹(即便用户点「跳过」也算 seen);version-suffix _v1 留给未来想强制再次 onboarding 的 bumpSettings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS fallback;用户实际要去 Device Care → Battery → Apps that aren't sleeping 加白名单——dialog 没法精准引导ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 系统 dialog,用户点 Allow 即把 isIgnoringBatteryOptimizations 翻 true(= Samsung 「不受限制」radio state,也是 onboarding 绿 ✓ 读的同一个 flag),零 Settings navigation。OEM autostart 第二步(Step 2)的 Samsung 落点仍是 fallback——OEM 自启动概念在 Samsung 不存在,所以 hasOemAutostart 对 Samsung 返 false、Step 2 根本不渲染、不存在「无可靠直链」candidatesForOem 即可,无 Dart 侧改动01-平台集成 §前台服务 PR #96 callout;这是 OS-level scan 路径问题,不在本服务的"OEM 权限引导"职责范围(此处留空,等待硬件/固件团队补充)