PDF Reader: Viewer, PDF Editor(Stackwise)v1.2.1 保活、拉活与通知逆向报告#
竞品包名:
com.pdfreader.pdf.viewer.pdfeditor
应用标签:PDF 阅读器:查看器,PDF 编辑器
样本版本:1.2.1(versionCode22)
静态分析与真机验证日期:2026-08-20
1. 结论先行#
- Stackwise 不是前台服务常驻型保活。其完整业务链路是:Firebase Remote Config 下发并启用
notification_configs后,BaseActivity.onPause()取消旧任务、以 WorkManager 排一条延迟ReminderWorker;任务到点后发本地“不活跃提醒”,然后再排下一轮。它用于离开 App 后召回,不是维持进程常驻。 - 该链路有明确远端开关、时长和文案模型:
is_enable、time_since_last_open_in_hour、contents[language_code].notifications[title, description]。APK 的安全默认值为 关闭 / 空文案 / 24 小时;因此没有从 APK 提取到可直接列举的营销标题或正文。 ReminderScheduler会先取消唯一工作名inactivity_reminder_work,再以ExistingWorkPolicy.REPLACE重排ReminderWorker。用户再次打开并退出主界面时,旧倒计时被替换,而非累计多条提醒。ReminderWorker按保存语言选远端文案,匹配不到时回退 English;以NotificationOpenTime % 列表长度轮换条目,创建高重要级inactivity_channel,在通知权限已授予时发布,并再次调度下一轮。这个变量在发布成功后递增,不能误写成真实“通知点击次数”。- 通知点击只使用
getLaunchIntentForPackage()回到 launcher。MainActivity只有收到带 PDF URI 的ACTION_VIEW才进入文件查看器;提醒 PendingIntent 不带 URI 或业务 route,所以这条链路只能证明“回 App”,不能证明会直达文件、编辑器或会员页。 - 真机本次启动后退到后台,仅观察到两个 DataTransport 网络 Job,未出现
ReminderWorker或inactivity_reminder_work的 WorkManager Job;且通知权限为拒绝。它说明本机这次没有生效的本地不活跃提醒,不能反推线上远端开关、人群或频控。 - 真机划掉最近任务后,任务条目、该包进程和该包 Service 均消失,包仍是
stopped=false;Force Stop 后包为stopped=true,30 秒内没有该包 PID 或 Service 输出。未见划卡自恢复、前台服务保活或绕过 Android Force Stop 的证据。 - APK 含 Firebase Messaging 基础组件与 FCM fallback channel,但 Manifest 中没有 Stackwise 自己的
FirebaseMessagingService子类。不能把“含 FCM”写成“已经验证 FCM 可拉活、可展示营销文案,或会启动上述提醒 Worker”。
2. 样本与证据口径#
| 项目 | 值 |
|---|---|
| 基础 APK | PDF/pdf-reader-viewer-editor-v1.2.1/apk/base.apk |
| ABI / density split | split_config.arm64_v8a.apk / split_config.xxhdpi.apk |
| base APK 大小 | 47,995,977 bytes |
| base SHA-256 | 5311bf856e3faf88706a8a479076218f76ec9da08df6107e6439d0ce9e4e03ea |
| arm64 split SHA-256 | 46194c2557c5e88a8d41889b5452ad71209136f361bf0613a2755a198fb0e878 |
| xxhdpi split SHA-256 | 4ed05e97872bc102e83ce8a32fbfe6df151cab87f98f0606d1f7af10a1d81780 |
| minSdk / targetSdk | 32 / 37 |
| Application / 主 Activity | com.tuansaw.pdf.SawApp / com.tuansaw.pdf.MainActivity |
| 动态环境 | 已授权 Android 16 / API 36 测试机;设备标识未写入本报告 |
本报告的证据层级:
- 静态事实:Manifest、DEX 方法、序列化字段或 APK 资源中直接可见的内容。
- 动态观察:本次测试机操作与
dumpsys、最近任务、进程及服务状态的结果。 - 合理推断:基于已还原调用链的技术/产品解释,不能代替服务端事实。
- 未验证:线上 Remote Config 值、推送下发、人群、频控、到达率和长时间系统行为。
3. 已还原的提醒与拉活调用链#
Firebase Remote Config
└─ notification_configs[可选版本后缀]
└─ 解析并缓存为 RemoteData.notification_configs
├─ is_enable(APK 默认 false)
├─ time_since_last_open_in_hour(APK 默认 24)
└─ contents[language_code].notifications[title, description]
│
▼
BaseActivity.onPause()(MainActivity 的父类)
└─ is_enable=true 才调用 ReminderScheduler
└─ 取消同名旧 Work → REPLACE 排 ReminderWorker(延迟 N 小时)
│
▼
ReminderWorker
├─ 当前语言无内容时回退 English
├─ 以 NotificationOpenTime % 列表长度轮换文案
├─ 创建高重要级 inactivity_channel
├─ POST_NOTIFICATIONS 已授予才发布本地通知
└─ 再次调用 ReminderScheduler,排下一轮
│
▼
用户点击通知 → getLaunchIntentForPackage() → App launcher
3.1 方法级代码锚点#
| 代码锚点 | 可直接看到的动作 | 链路角色 |
|---|---|---|
com.tech.core.BaseActivity.onPause() |
读取 RemoteManager.e.q.a(通知总开关);为真调用 ReminderScheduler.a(Context) |
用户离开 Activity 时的调度入口 |
com.tech.core.notification.ReminderScheduler.a(Context) |
取消 inactivity_reminder_work;构造 ReminderWorker 的 OneTimeWorkRequest;按小时设置 initial delay;同名唯一 Work 使用 REPLACE |
重置和排入延迟提醒 |
com.tech.core.notification.ReminderWorker.a() |
选语言、回退 English、取模轮换、建 channel、检查权限、发布并重新调度 | 到点后的展示与续排 |
com.tech.core.notification.NotificationHelper.a(Context, Intent, Notification) |
从 title / description 构建 NotificationCompat,写入 launcher PendingIntent,auto-cancel |
通知内容和点击回流 |
com.tech.core.remote.RemoteManager$updateRemoteData$1 |
从 Firebase Remote Config 取 notification_configs(可拼运行时版本后缀),按 NotificationConfigs serializer 解析,写回 RemoteData 缓存 |
远端字段来源闭环 |
com.tuansaw.pdf.MainActivity.w(Intent) |
仅对带 URI 的 ACTION_VIEW 取文件并导航 SawRoute.Viewer |
证明提醒 launcher intent 不会自行进入 PDF 查看页 |
ExistingWorkPolicy.a 在当前依赖中可还原为 REPLACE。因此“重新离开 App”会替换旧的唯一任务,而不是向系统堆积多个 24 小时 Worker。
4. Remote Config、频率与通知文案#
4.1 配置模型与默认安全值#
RemoteData.NotificationConfigs 的无参初始化方法直接给出默认值:
| 字段 | APK 默认值 | 用途 | 证据等级 |
|---|---|---|---|
is_enable |
false |
onPause() 的总开关 |
静态事实 |
contents |
空列表 | 按语言存放通知文案组 | 静态事实 |
time_since_last_open_in_hour |
24 |
Worker 首次/下一轮的延迟小时数 | 静态事实 |
contents[].language_code |
无本地默认项 | 与 App 保存语言匹配 | 静态事实 |
contents[].notifications |
空列表 | 该语言可用的提醒集合 | 静态事实 |
notifications[].title |
无本地默认项 | 通知标题 | 静态事实 |
notifications[].description |
无本地默认项 | 通知正文 | 静态事实 |
RemoteManager$updateRemoteData$1 会从 Firebase Remote Config 读取 notification_configs 加可选版本后缀的值,使用 NotificationConfigs serializer 解析;取不到值、值为空或解析失败时,会回退到上表所示本地默认对象,并把最终 RemoteData 写入偏好缓存。
这证明了配置来源和客户端回退行为;没有证明当前线上是启用、延迟为多少、发给哪些国家/版本,或某个用户实际命中了哪组内容。
4.2 有通知能力,但样本没有默认营销文案#
业务通知使用远端的 title 和 description,不是打包在资源里的固定模板。本样本 contents 默认为空,因此无法像含本地模板的竞品一样列出“真实会出现的标题/正文”。
能直接列举的固定文字仅是系统 channel 元数据:
| 项目 | 值 | 边界 |
|---|---|---|
| Channel ID | inactivity_channel |
本地不活跃提醒渠道 |
| Channel 名称 | Inactivity reminders |
固定 English 渠道名 |
| Channel 描述 | Reminds you to come back to the app |
固定 English 渠道描述 |
| importance | 4(HIGH) |
请求高重要级;用户或系统可降级 |
这不是用户可见的营销正文。真机还存在 fcm_fallback_notification_channel(importance 3),只能证明 Firebase SDK 初始化了 fallback channel,不能据此推断某条 FCM 文案或发信策略。
4.3 语言、轮换与权限边界#
Worker 的具体顺序如下:
- 读取保存语言;为空时使用 English。
- 在
contents中找同语言项;找不到时再找 English;仍没有则使用空列表。 - 用
NotificationOpenTime % notifications.size选择一条文案;列表为空时不发布。 - 创建 channel,并用 package 的 launcher intent 构造 PendingIntent。
POST_NOTIFICATIONS已授予时才交给NotificationManagerCompat发布;成功发布后NotificationOpenTime + 1。- 无论本轮是否有文案或权限,方法结尾均会重新调用
ReminderScheduler。
变量名 NotificationOpenTime 易误导:本代码中递增点在发布成功之后,不在通知点击回调中。它的已证实作用是条目轮换,不是已验证的点击归因。
5. WorkManager 调度与点击拉活边界#
5.1 调度语义#
ReminderScheduler 的顺序是“取消同名 Work → 创建 OneTimeWorkRequest → 取远端小时值(空值回退 24)→ REPLACE 入队”。Worker 本身继承 CoroutineWorker,这条业务方法中没有 setForeground() 或 ForegroundInfo 调用,也没有业务级网络、充电、空闲或精确闹钟约束。
因此它是一个请求在 N 小时后运行的本地延迟任务。实际执行时间仍受 Doze、后台配额、厂商限制和 Android 任务调度影响;它不等于“第 N 小时必达”,更不等于保活。
5.2 点击路径#
ReminderWorker 调用 PackageManager.getLaunchIntentForPackage(),随后 NotificationHelper 用 PendingIntent.getActivity() 写入通知;没有额外的 Screen、文件 URI、route 或业务 ID。
MainActivity.w(Intent) 对 ACTION_VIEW 的唯一已还原分支会读取 PDF URI 并进入 SawRoute.Viewer。提醒使用的是 launcher intent 而非该 ACTION_VIEW intent,所以通知点击只会把 App 带回默认启动流。冷启动、已有任务和启动流配置仍会影响最终画面,本次没有把它外推为固定页拉活。
6. 保活、启动入口与 FCM 的边界#
| 维度 | 已证实内容 | 不能写成的结论 |
|---|---|---|
| 前台服务 | Manifest 有 FOREGROUND_SERVICE 权限及 WorkManager 通用 SystemForegroundService;ReminderWorker 不调用前台执行 API |
不能称 Stackwise 正在用 FGS 常驻或保活 |
| 进程常驻 | 真机划掉任务后 PID、Service 均无该包条目 | 不能称存在划卡自恢复或常驻进程 |
| 本地提醒 | 已还原 onPause → OneTimeWork → ReminderWorker → 本地通知 → 续排 代码路径 |
不能称本机本次已收到该提醒,或线上已经全量开启 |
| 开机恢复 | 未声明 RECEIVE_BOOT_COMPLETED,未发现产品自定义 Boot receiver;Manifest 中仅有 WorkManager 的通用且 disabled 的 RescheduleReceiver |
未做重启实验,不能绝对断言所有 WorkManager 跨重启语义 |
| AlarmManager | 这条不活跃提醒使用 WorkManager | 不可据此断言整个 APK 及第三方 SDK 从不使用 AlarmManager |
| FCM 接收 | FirebaseInstanceIdReceiver 和库内 FirebaseMessagingService 已合并进 Manifest;DEX 中另有 AppsFlyer 的 listener 类,但未作为 Stackwise 自定义服务在 Manifest 注册 |
不能称 FCM 到达会启动 ReminderWorker、发营销通知或拉起前台服务 |
| FCM 点击 | fcm_fallback_notification_channel 存在 |
不能由 fallback channel 推出 payload、通知正文、深链或送达率 |
| Force Stop | 本次强停后 stopped=true,30 秒未观察到该包 PID 或 Service |
未发送真实 FCM,不用本次结果替代 Force Stop 下 FCM 的平台语义验证 |
特别说明:启动后曾观察到库内 Firebase Messaging Service 和 WebView sandbox service;它们的 startForegroundCount=0。这只能说明当时有 SDK/渲染服务绑定,不是本报告已证实的保活前台服务。
7. 真机验证结果#
| 场景 | 动态结果 | 可以得出的结论 | 不能外推的结论 |
|---|---|---|---|
| 通知权限与已有渠道 | POST_NOTIFICATIONS: granted=false;存在 FCM fallback channel |
本机当前不会由该 App 正常展示提醒;FCM SDK 已初始化 fallback channel | 不代表线上关闭提醒或不存在真实文案 |
| 启动 → 退到后台 | 只看到两个该包的 DataTransport 网络 Job;未见 ReminderWorker、inactivity_reminder_work 或该包 WorkManager system job |
本机本次退后台没有排入可见的不活跃提醒任务 | 不能确定是远端开关关闭、拉取未完成、远端内容为空或其他运行时条件未满足 |
| 启动后服务快照 | Firebase Messaging / WebView service 的 startForegroundCount=0 |
当时没有前台服务运行证据 | 不能代表每个时刻或所有设备均无其他服务 |
| 真实划掉最近任务 | 启动后该包在 Recent tasks;划除后 Recent tasks 不再包含该包,ps 与 dumpsys activity services 无该包输出;包为 stopped=false |
任务划除结束了当时应用运行态,未观察到自恢复 | 不排除远端 FCM 等外部事件日后短时启动进程 |
| Force Stop | stopped=true;30 秒观察期内无该包 PID 或 Service 输出 |
当前进程/服务没有回生证据 | 没有做小时级观察、重启或真实 FCM 到达实验 |
测试只启动、退后台、划卡和强停该竞品;未改通知权限、未清除数据、未读取用户文件、未修改任何竞品线上 Firebase/FCM 配置。
8. 与“保活/拉活”容易混淆的事项#
| 现象或组件 | 实际含义 | 不应混淆为 |
|---|---|---|
ReminderWorker |
延迟运行并展示本地提醒的 WorkManager Worker | 持续前台服务或进程保活 |
| Worker 的下一轮续排 | 到点后继续请求下一次延迟执行 | Android 保证的精确定时/永久任务 |
| FCM 基础组件 / fallback channel | 消息 SDK 与自动通知能力的基础设施 | 已验证的自定义推送文案、服务端频控或拉活链路 |
| DataTransport 网络 Job | Firebase/Google 传输与遥测调度 | 用户召回 Worker 或常驻保活 |
| launcher PendingIntent | 用户点击后打开应用默认启动流 | 带文件/功能上下文的深链拉活 |
FOREGROUND_SERVICE 权限 |
声明了可使用的系统能力 | 当前有正在运行的前台服务 |
9. 未验证项与继续取证条件#
- 当前线上
notification_configs的开关、延迟、语言文案、国家/版本分流和频控。需要受控读取 Remote Config 或经授权的服务端证据,不能从 APK 默认值推断。 - 真实提醒的标题、正文、展示时间、点击后首屏。需要在不篡改竞品配置的前提下,由已启用的受控测试用户等待/触发,并在通知权限允许时记录。
- Doze、后台受限、重启、不同 OEM 设备上的实际 WorkManager 执行时机。本次没有跨小时等待、重启或机型矩阵测试。
- FCM 的 data/notification payload、是否 SDK 自动展示、服务端深链参数,以及 Force Stop 后到达行为。当前没有授权向竞品项目发送真实 FCM。
shiny_notification_channel的创建方和用途。它在设备渠道列表中存在,但在本次已还原的提醒方法和基础 DEX 字符串中没有形成可归因的业务调用链,不能纳入提醒结论。