现状是一件保活都没做:Android 12+ 会把 cached 进程冻结,进程级共享的那条 WS 连接 (AppContainer.chatSocket)久了就断。这一批只解决「进程别被冻」这一件事,连接本身的生死 仍然归 SessionsViewModel(登录连、登出关),本服务不碰它、也不订阅任何帧。 - core/keepalive/KeepAliveService:dataSync 型前台服务,职责只有让进程活着。常驻一条 LOW 级静默通知(不响不震、setOnlyAlertOnce、点一下回 App),渠道只在服务真的起来时才建。 - core/keepalive/KeepAlivePolicy:起停判据抽成纯函数。shouldKeepAliveRun 三个条件缺一不可 (开关开 / 已登录 / 通知权限在);keepAliveAction 再补上「开关开着但登录态未定(自举中) 一律不动」——不补的话热启动会看到通知闪一下,而中间那段进程正好没人保护。判据刻意**不**查 「现在跑着没有」:那份进程内记忆会被「系统收掉服务却不走 onDestroy」写坏,于是该起的时候 一声不响地没起(保活最不能出的错);重复 start/stop 本身是无害的。 - CyreneApp:全 App 唯一一处按判据起停,开关或登录态一变就重判一次 ⇒「启动时开着且已登录就 在跑」「登出即停」「关开关即停」三件事不需要各自一条路径。 - 设置页新增一行(默认关),文案如实写清代价(常驻一条通知 + 多耗一点电)与当下真实状态; Android 13+ 申请 POST_NOTIFICATIONS,被拒则开关弹回关闭并说明原因,绝不假装开着。 实现落在新文件 KeepAliveRow.kt:SettingsScreen.kt 只加一行调用(放在「关于」之前,不碰 最底下的登出块),避开 W52C 正在编辑那一段。 - KeepAliveService.onTimeout:targetSdk 37 下 dataSync 前台服务 24 小时内累计跑满 6 小时会被 系统回调,不自己停就 ANR 崩掉整个 App —— 照办 stopSelf,不绕(绕它要么换类型要么加心跳, 都超出本功能边界)。 - Manifest:FOREGROUND_SERVICE / FOREGROUND_SERVICE_DATA_SYNC / POST_NOTIFICATIONS + service (exported=false、foregroundServiceType=dataSync)。 测试:新增 3 个文件 13 条 —— 默认关、三条件缺一不可、四入参全表一致、文案四档各不 相同且「跑不了时一个字都不提已开」、渠道重要级只许 LOW/MIN。基线 93 suite/1205 tests 全部保持 通过,本树 96 suite/1218 tests/0 失败。
100 lines
5.1 KiB
XML
100 lines
5.1 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||
|
||
<!--
|
||
「后台保持连接」用到的三个权限(设置页那个开关,默认关):
|
||
|
||
- FOREGROUND_SERVICE:起前台服务本身。
|
||
- FOREGROUND_SERVICE_DATA_SYNC:前台服务的类型权限(API 34+ 与 android:foregroundServiceType
|
||
配合)。选 dataSync 是因为这条服务保的是一条**长连接**——保持与后端的实时同步,
|
||
既不是 mediaPlayback 也不是 location,声明成别的类型才是说谎。
|
||
- POST_NOTIFICATIONS:前台服务必须有一条通知,而 minSdk 33 ⇒ 它恒是运行时权限。
|
||
用户拒绝时开关**打不开**(判据见 core.keepalive.shouldKeepAliveRun),本端不偷偷跑。
|
||
-->
|
||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||
|
||
<application
|
||
android:name=".app.CyreneApplication"
|
||
android:allowBackup="false"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:label="@string/app_name"
|
||
android:networkSecurityConfig="@xml/network_security_config"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:supportsRtl="true"
|
||
android:theme="@style/Theme.Cyrene">
|
||
|
||
<!--
|
||
launchMode="singleTop":App 已经开着时从别的 App 分享过来,系统不再新建一个实例,
|
||
改成回调 onNewIntent(见 MainActivity)。不写它就会起第二个 Activity 实例、
|
||
两个实例各持一套 ViewModel / socket。
|
||
-->
|
||
<activity
|
||
android:name=".app.MainActivity"
|
||
android:exported="true"
|
||
android:label="@string/app_name"
|
||
android:launchMode="singleTop"
|
||
android:windowSoftInputMode="adjustResize">
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.MAIN" />
|
||
<category android:name="android.intent.category.LAUNCHER" />
|
||
</intent-filter>
|
||
|
||
<!--
|
||
分享入口:只声明本端真能落地的两类 MIME(纯文本 / 图片)。
|
||
刻意不声明 */*——对话路径上今天没有「文件」这个表示(协同文档 02/F1 的三缺),
|
||
声明了就等于给出一个点进去必然失败的入口。标签沿用应用名(活动不设自己的
|
||
label / icon 时,分享菜单取的就是 Cyrene 的名字与启动图标)。
|
||
-->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.SEND" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<data android:mimeType="text/plain" />
|
||
<data android:mimeType="image/*" />
|
||
</intent-filter>
|
||
|
||
<!-- 多图分享只收图片:多条纯文本的 EXTRA_TEXT(SEND_MULTIPLE + text/plain)本端不接。 -->
|
||
<intent-filter>
|
||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||
<category android:name="android.intent.category.DEFAULT" />
|
||
<data android:mimeType="image/*" />
|
||
</intent-filter>
|
||
</activity>
|
||
|
||
<!--
|
||
附件预览的「用其它应用打开 / 保存」出口:`cacheDir/documents/` 下的文件要交给
|
||
系统里别的 App 读,就必须是 content:// 而不是 file://——API 24 起把 file:// 交出去
|
||
会直接抛 FileUriExposedException(且 StrictMode 下同样致命)。
|
||
路径范围见 res/xml/file_paths.xml:只暴露 cacheDir/documents/ 这一层,
|
||
exported="false" + 按次授权(grantUriPermissions)保证这份缓存不对外常开。
|
||
-->
|
||
<provider
|
||
android:name="androidx.core.content.FileProvider"
|
||
android:authorities="${applicationId}.fileprovider"
|
||
android:exported="false"
|
||
android:grantUriPermissions="true">
|
||
<meta-data
|
||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||
android:resource="@xml/file_paths" />
|
||
</provider>
|
||
|
||
<!--
|
||
「后台保持连接」的前台服务(设置页开关控制,默认关)。
|
||
|
||
exported="false":它没有任何对外接口(onBind 返回 null),只由本进程 start/stop。
|
||
|
||
foregroundServiceType="dataSync":这条服务的职责是让进程活着,从而让那条**进程级共享**
|
||
的实时连接(AppContainer.chatSocket)不被 Android 12+ 的 cached 进程冻结掐断——
|
||
语义上就是「后台数据同步」。它不自己去建立 / 关闭任何连接,连接的主人是
|
||
SessionsViewModel(见 KeepAliveService 的类注释)。
|
||
-->
|
||
<service
|
||
android:name=".core.keepalive.KeepAliveService"
|
||
android:exported="false"
|
||
android:foregroundServiceType="dataSync" />
|
||
|
||
</application>
|
||
|
||
</manifest>
|