PDF Reader: View All PDFs(Lumio)v1.2.8 保活、拉活与通知逆向报告#
竞品包名:
com.espublishing.pdfreader.viewer.editor
应用标签:PDF Reader: View All PDFs
样本版本:1.2.8(versionCode30)
静态分析与真机验证日期:2026-08-20
1. 结论先行#
- 该 APK 不是前台服务常驻型保活。已还原的业务链路是:远端配置启用后,
MainActivity.onPause()排一个延迟 WorkManager 任务;任务到点后展示一条本地“不活跃提醒”通知,并自行排下一轮。它的目标是离开 App 后的用户召回,不是维持进程常驻。 - 该链路有明确总开关和频率参数:
notification_configs.is_enable为总开关,time_since_last_open_in_hour为首次延迟,APK 默认值是 false / 24 小时。用户再次进入并离开主页面会取消同名旧任务并重新从该延迟开始计时。 ReminderWorker是可执行的业务 Worker,不是只打包了 WorkManager。它按已保存的语言寻找远端contents,优先当前语言、失败回退 English;从该语言通知列表按序轮换标题和正文,创建inactivity_channel后发布本地通知。- 点击通知只取包的 launcher intent 并回到 App,并没有在这条链路中看到文件、编辑器或会员页等目标深链参数。因此它是“回 App”召回,不能证明点击后一定进入某个特定业务页面。
- 本次真机在“启动 → 后台”窗口内只看到 Firebase/传输 SDK 的网络 Job,未出现
ReminderWorker的 WorkManager 系统任务;所以本机本次没有观察到已生效的不活跃提醒。原因可能是远端开关为关、远端尚未完成下发,或当前首启路径未满足调度时机,不能据此反推线上全量策略。 - 真机真实划掉最近任务后,Lumio 任务条目、进程和 Service 均消失;
startForegroundCount=0。Force Stop 后包进入stopped=true,Service、Job、进程在 30 秒观察期内均未恢复。它没有绕过 Android Force Stop 的保活证据。 - APK 有 FCM 基础组件,但当前基准 DEX 未见
com.longct...自定义FirebaseMessagingService来把到达推送接到上述提醒链路。不能把“含 FCM”写成“FCM 可拉活并展示营销通知”。
2. 样本与证据口径#
| 项目 | 值 |
|---|---|
| 基础 APK | PDF/pdf-reader-viewer-v1.2.8/apk/base.apk |
| ABI / density split | split_config.arm64_v8a.apk / split_config.xxhdpi.apk |
| base APK 大小 | 40,967,727 bytes |
| base SHA-256 | b016c8522009e1ff6a22e1cc703dc8bf6e4d3c2796dfcaf12b32cf3dda26789d |
| arm64 split SHA-256 | d19b7610d3e7303f19981d3fef44be7815cb8e178e6faccc7b27ffa9a8ba4832 |
| xxhdpi split SHA-256 | 6a12228ba045a29188d2beab56bed347195bbbe4e9853849df57aa0c1155a5f5 |
| minSdk / targetSdk | 32 / 37 |
| 主 Activity | com.longct.pdfreader.app.MainActivity |
| 动态环境 | 已授权 Android 16 / API 36 测试机;设备标识未写入报告 |
本报告将证据分为四类:
- 静态事实:Manifest、DEX 代码、序列化字段或 APK 资源中直接可见的内容。
- 动态观察:本次测试机操作与
dumpsys、任务栈、进程状态的结果。 - 合理推断:由已证实调用链得出的产品/技术解释,不能替代服务端事实。
- 未验证:远端实际配置、发信、人群、频控、到达率或长期系统行为。
3. 已还原的业务调用链#
远端 RemoteData.notification_configs
├─ is_enable(默认 false)
├─ time_since_last_open_in_hour(默认 24)
└─ contents[language_code].notifications[title, description]
│
▼
MainActivity.onPause()
└─ is_enable=true 时:取消同名旧 Work
→ 以 REPLACE 方式排 ReminderWorker(延迟 N 小时)
│
▼
ReminderWorker.doWork()
├─ 取保存语言;当前语言无内容时回退 English
├─ 以 NotificationOpenTime % 列表长度选一条
├─ 创建高重要级 channel:inactivity_channel
├─ 已获 POST_NOTIFICATIONS 才发布本地通知
└─ 再次排 ReminderWorker(形成“到点后下一轮”)
│
▼
用户点击通知 → getLaunchIntentForPackage() → 回到 App launcher
3.1 DEX 代码锚点#
| 代码锚点 | 可直接看到的动作 | 在链路中的位置 |
|---|---|---|
com.longct.pdfreader.app.MainActivity.onPause() |
读取 rh6.e.q.a(通知配置开关);为真调用 bt2.w(Context) |
用户离开主页面时重置倒计时 |
bt2.w(Context) |
取消 inactivity_reminder_work;构造 com.tech.core.notification.ReminderWorker;以 hours 转换后的 initial delay 和 REPLACE 排入唯一 Work |
业务排程函数 |
com.tech.core.notification.ReminderWorker.a() |
选语言、取通知、创建 channel、检查 POST_NOTIFICATIONS、发布并再次调用 bt2.w() |
到点后的本地召回与续排 |
bk1.r(Context, Intent, kh6) |
用 kh6 的远端 title/description 构造通知,并附 launcher PendingIntent |
通知展示与点击回流 |
com.tech.core.remote.RemoteData$NotificationConfigs$$serializer |
定义 is_enable、contents、time_since_last_open_in_hour 三个字段 |
远端数据模型证据 |
这是一个“本地延迟通知 + 打开 App 重置倒计时”的循环,不包括服务保活、开机自启或进程死亡后立刻补拉。系统对延迟 Work 的实际运行时机仍可施加 Doze、配额、后台限制等约束。
4. 通知配置与远端数据证据#
4.1 配置模型#
com.tech.core.remote.RemoteData.NotificationConfigs 的 Kotlin 序列化字段可直接还原为:
| 字段 | APK 默认值 | 在链路中的用途 | 证据等级 |
|---|---|---|---|
is_enable |
false |
MainActivity.onPause() 的总开关 |
静态事实 |
contents |
空列表 | 按语言存放通知文案组 | 静态事实 |
time_since_last_open_in_hour |
24 |
ReminderWorker 初始延迟(小时) |
静态事实 |
contents[].language_code |
无本地默认项 | 与当前保存语言比对 | 静态事实 |
contents[].notifications |
空列表 | 某语言可用通知集合 | 静态事实 |
notifications[].title |
无本地默认项 | 通知标题 | 静态事实 |
notifications[].description |
无本地默认项 | 通知正文 | 静态事实 |
初始化代码会从 SharedPreferences 的 RemoteData 读取序列化对象;拉取成功路径也会将更新后的对象以同名键写回,并记录 “Remote config fetched successfully”。APK 同时注册 Firebase Remote Config 组件,但本次只证明它有一条命名为 RemoteData 的拉取、解析和本地缓存路径,没有把 Firebase Remote Config 与这一字段逐项建立唯一数据源关系。
4.2 文案不是本地硬编码模板#
ReminderWorker 先读取已保存的 Language;匹配不到语言内容时查 English;找不到内容或通知列表为空则不展示。与另一份 PDF Reader 报告中“本地 9 组可达 FCM 模板”不同,Lumio 这条提醒路径的业务标题/正文来自远端 contents,APK 没有给出可列举的默认营销文案。
APK 本地固定的仅是 channel 元数据:
| 项目 | 值 | 含义边界 |
|---|---|---|
| Channel ID | inactivity_channel |
用于不活跃提醒的本地通知渠道 |
| Channel 名称 | Inactivity reminders |
固定 English 系统渠道名 |
| Channel 描述 | Reminds you to come back to the app |
固定 English 系统渠道描述 |
| importance | 4(HIGH) |
请求高重要级,不保证用户/系统不会降级 |
5. WorkManager 调度细节#
5.1 排程入口#
MainActivity.onPause() 先读取 notification_configs.is_enable;为真才调用调度函数。调度函数执行顺序如下:
- 取消唯一工作名
inactivity_reminder_work。 - 创建
ReminderWorker的 OneTimeWorkRequest。 - 取
time_since_last_open_in_hour,空值回退24,转换为毫秒初始延迟。 - 以
ExistingWorkPolicy.REPLACE重新排入同名唯一任务。
这意味着只要用户重新进入再离开主页面,旧倒计时就会被替换;代码没有在该 Worker 的创建处设置网络、充电、空闲、前台执行等业务约束。它是请求延后 N 小时,而非保证第 N 小时准点通知。
5.2 Worker 的显示、轮换与下一轮#
ReminderWorker 继承 CoroutineWorker,不是前台 Worker,也没有在其业务代码中调用 setForeground()。到点后:
- 以
NotificationOpenTime % notifications.size选择该语言列表中的一项。 - 使用
title和description生成本地通知;通知设为 auto-cancel。 POST_NOTIFICATIONS被授予时才调用发布;成功路径将NotificationOpenTime加一。- 无论本轮是否找到文案,方法尾部都会再次调用同一排程函数。
字段名 NotificationOpenTime 容易误读:当前代码是“展示后递增,用于下一次取模轮换”,没有看到它等待用户点击通知后才递增。因此不能把它称为“通知点击次数”或真实打开次数。
5.3 点击路由#
Worker 取得 getLaunchIntentForPackage(),再以 PendingIntent.getActivity() 写入通知。可见 flag 用于清理/复用已有 launcher task,但未携带文件 URI、工具名、会员页或自定义 route。结论是“通知点击回 App”;点击后的最终首屏还会受已有任务状态、冷启动和 App 自身路由影响,未做逐页真机验证。
6. 保活、拉活与 FCM 的边界#
| 维度 | 已证实内容 | 不能写成的结论 |
|---|---|---|
| 前台服务 | Manifest 有 WorkManager 的通用 SystemForegroundService 和 FOREGROUND_SERVICE 权限;ReminderWorker 自身非 FGS |
不能称 Lumio 正在用前台服务保活 |
| 进程常驻 | 真实划卡后 PID 消失,Service 为空 | 不能称有划卡自恢复或常驻进程 |
| 本地通知 | 已还原 onPause → OneTimeWork → ReminderWorker → 本地通知 完整静态链路 |
不能称本机本次已实际收到该通知;远端开关未在本机观察到生效 |
| 开机恢复 | Manifest 未见 RECEIVE_BOOT_COMPLETED 或应用自定义 Boot receiver |
未做重启测试,不能绝对断言 WorkManager 内部跨重启行为 |
| AlarmManager | 此业务提醒链路使用 WorkManager | 不可据此断言整个 APK 完全不用 AlarmManager(DataTransport 等 SDK 可有自身调度) |
| FCM | Manifest 有 FirebaseMessagingService;基准 DEX 对该类的直接子类引用是 AppsFlyer listener,未见 com.longct... 自定义消息处理类 |
不能称收到 FCM 会启动 ReminderWorker、发营销通知或拉起前台服务 |
| Force Stop | 真机 Force Stop 后包 stopped=true,进程/Service/Job 为空,30 秒无重启 |
未对 stopped 状态发送真实 FCM,不能用本次结果单独证明 FCM 在 Force Stop 下的所有平台语义 |
特别说明:Firebase Messaging Service 在某个早期快照中处于绑定状态,但 startForegroundCount=0;它是消息 SDK 服务,不是本报告已证实的保活前台服务。其后真实划掉任务时,该服务也随应用进程消失。
7. 真机验证结果#
| 场景 | 动态结果 | 可以得出的结论 | 不能外推的结论 |
|---|---|---|---|
| 启动与短时运行 | 启动页后出现全屏广告;服务快照有 Firebase Messaging、WebView 沙盒 | APK 在该时点运行过消息/WebView SDK | 不代表这些服务是保活逻辑或每次启动都出现广告 |
| 启动后置后台 | JobScheduler 仅观察到 DataTransport 的网络条件 Job,未见 ReminderWorker / WorkManager system job |
本机该窗口没有实际排入不活跃提醒任务 | 不能确认是远端开关关闭、拉取未完成还是首启路径条件未满足 |
| 后台服务快照 | Firebase Messaging 和 WebView 沙盒的 startForegroundCount=0 |
当前没有前台服务运行证据 | 不能证明长期后台策略或所有运行时刻均无 FGS |
| 真实划掉最近任务 | 最近任务中的 Lumio 卡片被划除;任务栈不再有该包;Service 为 (nothing);pidof 无输出 |
划卡会结束当前 Lumio 进程,未观察到划卡自恢复 | 不排除远端 FCM 等外部事件日后启动短时进程 |
| Force Stop | 先启动后执行 Force Stop;包状态为 stopped=true;Service / Job / PID 均为空 |
Android Force Stop 切断当前客户端调度和运行态 | 没有做 Force Stop 后真实 FCM 到达实验 |
| Force Stop 后 30 秒 | 仍无 Lumio PID | 此观察窗内没有自启/回生 | 不是长期小时级到达率测试 |
未申请 MANAGE_EXTERNAL_STORAGE,未读取任何用户文件;也未改动竞品线上配置、通知权限或用户数据。
8. 首启广告观察(与本专项的关系)#
本次首次流程观察到:启动页约 16 秒后出现一条全屏广告,关闭后进入语言选择和三步引导。它说明首启漏斗可能被广告打断,但和不活跃提醒是两条不同路径:
- 全屏广告:本次动态观察,受网络、广告填充和频控影响。
- 不活跃提醒:APK 内已还原的本地 WorkManager 调度代码,受远端开关、通知权限与系统调度影响。
不能因为二者都依赖后台/远端能力,就把广告预加载、FCM、通知召回和进程保活混为一类能力。
9. 对我方的可借鉴与风险边界#
可以借鉴#
- 对“用户离开后提醒”使用唯一 OneTimeWork,并在用户重新活跃时
REPLACE倒计时;语义清楚,也避免多条重复通知。 - 将开关、延迟、语言文案分层配置,但需在客户端设置安全默认值、上限、格式校验和无内容兜底。
- 通知点击应使用经过验证的内部 route,必要时附带真实业务对象 ID;仅回 launcher 会让效果归因和用户回流路径都不清楚。
- 埋点需拆开:任务排入、系统实际执行、权限拒绝、通知展示、点击、回到页面、后续转化。它们不是同一件事。
不建议照搬#
- 不把 WorkManager 的延迟任务宣传为“保活”或“定时必达”;系统可以推迟或取消。
- 不用无真实业务事件支撑的文案做高优先级重复提醒;本样本变量名虽叫“inactivity”,仍需要我方明确用户授权、频率上限和退订入口。
- 不用前台服务权限、FCM 依赖或 WorkManager 组件存在来代替真机验证;三者分别只说明不同层的可能性。
- 不将 Force Stop、通知权限关闭、系统后台限制视为异常;它们是 Android 的产品边界,应在验收标准中单列。
10. 未验证项与后续取证条件#
- 当前线上
is_enable、实际延迟、语言文案、人群、频控和是否按国家/版本分流;需要受控网络抓包或获得授权的服务端配置,不能从 APK 猜测。 ReminderWorker在该样本本机的真实出通知时间;需要远端启用后,以可控短延迟复现,且不得篡改竞品数据或配置。- 通知权限拒绝、重新授权、Doze、后台受限、重启和 OEM 系统下的实际执行时机;本次未做跨小时等待或重启实验。
- FCM 到达后是否有 SDK 自动展示通知、服务端 data/notification payload 策略及 Force Stop 状态下行为;当前没有授权向竞品项目发送真实消息。
RemoteData拉取端的实际服务端来源。APK 可见 Firebase Remote Config 组件和独立RemoteData缓存路径,但二者的字段映射尚未静态闭环。