跪拜 Guibai
← Back to the summary

Rebuilding Android Launch and Persistence After Your Process Gets Killed

After Being Killed: I Redid Android's "Launch + Persistence"

From clicking a link in a text message, to whether the process is still alive ten minutes after screen lock, it's a whole system in between. This article breaks it down from start to finish, and also pastes the prompts I used with Trae.

Recently I needed a feature that continuously reports in the background, and I got tripped up by the same problem several times. When a user clicks a link from a text message, the page opens correctly, but pressing back returns to the desktop; switching to coming from a notification, parameters mysteriously get lost; after upgrading targetSdk to 34, the foreground service throws SecurityException on startup, with only a line "Starting FGS requires a valid type" in the log. I stared at the Manifest for a long time and couldn't find where the mistake was.

Later I understood: these aren't three bugs, it's one thing not designed well: how to launch the App, and how to make it continue working as expected after being launched, should be one architecture. If handled separately, things will leak at the three boundaries: cold start, warm start, and process being recycled.

Let me be upfront: this article doesn't discuss old methods like dual-process guardianship, 1-pixel Activity, or silent audio. Not because I'm being noble, but because they genuinely don't work. After Android 8, the system has blocked them one by one, and domestic ROMs' power-saving strategies are even more aggressive than stock. The payoff for all that effort is often just "a few more minutes of life," at the cost of being on the battery drain list, bad reviews, and being called out during compliance checks. The path I finally took is: make background tasks visible and stoppable, and let processes recover from breakpoints when they die.

Also, to set the environment so I don't repeat it each section: Kotlin, minSdk 26, targetSdk 35, and also need to account for Android 16 behavior; only using AndroidX, no third-party SDKs; permission dialogs are native AlertDialog. The example business is a feature that needs to continuously sync data in the background once enabled. It's a common enough scenario, but it ties together both the launch and persistence paths.

First, Distinguish Three Terms

Wake-up, persistence, and keep-alive are often used interchangeably, but in the system's eyes they are three different things.

Term What it solves How the system sees it Main APIs What counts as done right
Wake-up From "process doesn't exist / in background" to "has UI or a task running" Component start, process creation, task stack management Intent, PendingIntent, App Links, startForegroundService Page opens correctly, parameters not lost, back stack reasonable
Persistence After user leaves UI, task continues as expected Process priority, background execution quota Service + startForeground, WorkManager No interruption or recoverable, notification visible, user can stop
Keep-alive (old meaning) Keep process from being killed Fighting with LMK, Doze, vendor power-saving Dual-process, 1-pixel, mutual pull... Unreliable and non-compliant

There's also another distinction that's easy to overlook: a process being "alive" has three different states.

State Phenomenon Application.onCreate Activity Wake-up needs to do
Cold start Tap icon / link / push Executes Create new Full init, parse Intent, rebuild back stack
Warm start Return from recents, Activity recycled Doesn't execute Create new Restore savedInstanceState, then parse Intent
Hot start Return from background, singleTask reuse Doesn't execute Reused Go through onNewIntent, don't push another page

The most common online issue I've seen is only reading intent.data in onCreate. On hot start, the Activity is reused and goes through onNewIntent, so link parameters are silently lost without any error. In the subsequent code, both entry points call the same dispatch.

Before starting, I set a few principles for myself: the process can die at any time, that's a premise not an accident; anything that can't be lost must be persisted to disk; all launch paths must have a single entry point; what the background is doing must be visible to the user and storable; every system-version-dependent behavior must be labeled with API level. The first four determine all subsequent trade-offs, and the last one has saved me multiple times.

The Final Design

flowchart TB
    subgraph T["① 触发层 Trigger"]
        T1["Launcher 图标"]
        T2["Deep Link / App Links"]
        T3["通知点击 PendingIntent"]
        T4["厂商推送 / FCM"]
        T5["BOOT_COMPLETED / Job / Alarm / Geofence"]
    end
    subgraph L["② 唤起层 Launch & Router"]
        L1["Intent 解析 + 可信校验"]
        L2["DeepLinkParser → Route"]
        L3["闸门: 登录 / 隐私弹窗 / 权限"]
        L4["Navigator + TaskStackBuilder 栈重建"]
    end
    subgraph S["③ 会话与常驻层 Session"]
        S1["ResidentService (FGS)"]
        S2["常驻通知 (用户可见 / 可停止)"]
        S3["任务状态机 StateMachine"]
        S4["StateStore 持久化 (幂等/断点)"]
    end
    subgraph D["④ 调度与恢复层 Recovery"]
        D1["WorkManager / JobScheduler"]
        D2["厂商推送唤醒"]
        D3["START_STICKY + 用户再次打开"]
        D4["电池优化 / 自启动白名单引导"]
    end
    subgraph O["⑤ 可观测层 Observability"]
        O1["结构化日志 + 埋点"]
        O2["存活时长 / 恢复次数 / 耗电"]
        O3["崩溃与 ANR 监控"]
    end
    T --> L
    L --> S
    S --> D
    D -. "进程被杀后重新拉起" .-> L
    S --> O
    L --> O
    D --> O

If Mermaid doesn't render, here's a text version:

[外部触发] 图标 / H5链接 / 通知 / 推送 / 开机 / 定时任务
      │
      ▼
[唤起层]  Intent → 可信校验 → 统一路由 → 闸门(登录/合规/权限) → 页面 + 返回栈重建
      │
      ▼
[常驻层]  前台服务 + 常驻通知 + 状态机 + 断点持久化
      │
      ▼
[恢复层]  WorkManager / 推送 / START_STICKY → 状态恢复 → 幂等续跑
      │
      └────────────► [可观测层] 日志 / 存活指标 / 崩溃监控

The layering is actually simple: it answers five questions: who called me, how to get in, what to do while here, how to return if gone, and how well it actually did. Splitting it isn't for drawing fancy diagrams, but because each layer can change its implementation independently—for example, switching push channels from one vendor to another only touches the trigger layer; router and state machine don't change a line.

Layer Key classes Only responsible for Don't let it do
Trigger BootReceiver, PushReceiver Turn external events into a controlled Intent Don't run heavy logic in broadcast, don't open pages directly
Launch DeepLinkParser, LaunchRouter, Navigator Validate, route, gate, open page No network requests, no scattered startActivity
Persistence ResidentService, NotificationFactory, TaskDispatcher Hold a foreground service, maintain state machine Don't hold Activity, don't write unbounded loops
Recovery ResidentWatchdogWorker, StateStore, RecoveryCoordinator Idempotent recovery from persisted state after process death Don't rely on restart in onTaskRemoved
Observability ResidentLogger, ResidentMetrics Record launch source, survival time, recovery count, failure reasons Don't collect unrelated personal info

Three chains are the focus.

Chain A: From link cold start to correct page

sequenceDiagram
    participant U as 用户 / 外部 App
    participant S as Android 系统
    participant A as Application
    participant R as LaunchRouter
    participant G as GateKeeper
    participant N as Navigator
    participant P as 目标页面
    U->>S: 点击 https://app.example.com/task/42
    S->>S: 校验 assetlinks.json → verified
    S->>A: 冷启动进程,onCreate()
    A->>R: dispatch(intent)
    R->>R: DeepLinkParser 校验 host/参数 → Route.TaskDetail(42)
    R->>G: 是否需要登录 / 隐私弹窗 / 权限
    G-->>R: 通过(或先弹窗,把 Route 暂存到 PendingRoute)
    R->>N: open(route, buildBackStack=true)
    N->>P: TaskStackBuilder 合成 首页→详情 返回栈
    Note over P: 返回键回到首页,不是直接退桌面

Chain B: User clicks "Start"

sequenceDiagram
    participant U as 用户
    participant UI as 页面
    participant G as GateKeeper
    participant S as ResidentService
    participant N as 通知栏
    participant D as TaskDispatcher
    participant DB as StateStore
    U->>UI: 点击「开始」
    UI->>G: 先弹权限用途说明
    G-->>UI: 同意 → 申请 POST_NOTIFICATIONS / 定位权限
    UI->>S: startForegroundService(ACTION_START)
    S->>N: startForeground(id, notification, typeMask)
    S->>DB: 状态置 RUNNING,写入任务队列
    S->>D: resumePendingWork() 幂等执行
    Note over N,D: 通知常驻;用户点「停止」→ ACTION_STOP → stopSelf

Chain C: After process is recycled

sequenceDiagram
    participant K as 系统 LMK / 厂商省电策略
    participant S as ResidentService
    participant DB as StateStore
    participant W as WorkManager / 推送
    participant R as RecoveryCoordinator
    participant N as 通知栏
    K->>S: 进程被杀(onDestroy 可能不回调)
    Note over DB: 进度在被杀之前已 checkpoint 落盘
    W->>R: 周期任务触发 / 推送到达 / 用户再次打开 App
    R->>DB: 读取 lastState + 未完成任务队列
    R->>R: 判断是否仍在用户授权窗口内
    R->>S: startForegroundService(ACTION_RESUME)
    S->>N: 重新 startForeground + 恢复通知
    S->>DB: 从断点幂等续跑(去重 key 防重复执行)

Chain C I've revised three times. The first version relied entirely on START_STICKY, occasionally survived on test devices, but not on another; the second version restarted the service in onTaskRemoved, which was blocked by ROMs; finally I changed to the current one: four paths—START_STICKY, WorkManager fallback, push arrival, and user reopening—converge to the same RecoveryCoordinator, using sessionId and dedupeKey to ensure that no matter which path arrives first or several arrive simultaneously, the task runs only once.

State machine:

            ACTION_START              ACTION_PAUSE
  ┌──────┐ ───────────► ┌─────────┐ ───────────► ┌────────┐
  │ IDLE │              │ RUNNING │              │ PAUSED │
  └──────┘ ◄─────────── └─────────┘ ◄─────────── └────────┘
       ▲      ACTION_STOP       │  ▲                   │
       │                       │  │ ACTION_RESUME     │
       │                 进程被杀│  └───────────────────┘
       │                       ▼
       │                  ┌────────────┐
       └────────────────── │ RECOVERING │ ──► 校验授权窗口 ──► RUNNING / IDLE
                          └────────────┘

The state itself must be persisted; each transition writes a checkpoint: state, taskQueue, lastProgress, updatedAt, sessionId, plus userAuthorizedUntil. The last field was added later. Earlier versions didn't have it; when the user clicked "Stop", the watchdog quietly pulled it up and ran another round—looked fine functionally, but not compliant.

Wake-up: How a Link Reaches the Page

First, list all the entry points that can wake me up, and their boundaries.

Entry Who initiates Can it be background Where it might fail Used for
Launcher icon User — None Regular launch
Custom Scheme H5 / SMS / other App Yes (if initiated by foreground App) May be hijacked, shows chooser; 12+ must be explicit exported Internal navigation, legacy compatibility
App Links (https verified) Browser / SMS / email Yes Requires assetlinks.json verification; 12+ without verification goes to browser; server must not redirect External sharing, SMS traffic, QR scan
Notification click PendingIntent User taps notification This is the most stable legal way for background to open a page 12+ needs FLAG_IMMUTABLE / FLAG_MUTABLE; 14+ background PendingIntent send needs explicit opt-in Background task status, message alerts
Vendor push / FCM System-level push channel Yes (system pulls process) Arrival doesn't mean can directly open page IM, monitoring alerts
BOOT_COMPLETED System broadcast Yes Implicit broadcast restricted; 15+ some FGS types cannot start from boot broadcast Reschedule after reboot
WorkManager / JobScheduler / AlarmManager System scheduler Yes Min period 15 min; exact alarm needs SCHEDULE_EXACT_ALARM etc. Fallback checks, delayed tasks
Geofence / ActivityRecognition System service Yes Requires while-in-use permissions and corresponding FGS type Location business
Explicit Intent to other App This App Depends 11+ needs <queries> for package visibility Cooperative app navigation
Background direct startActivity This App 10+ basically prohibited Only few exemptions like visible window, foreground service notification Don't use; send notification instead

App Links: Looks Configured, But Not Working

This is where I wasted the most time. In the Manifest, I split the https and custom scheme into two filters because writing them together can produce unexpected matching combinations:

<activity
    android:name=".ui.MainActivity"
    android:exported="true"
    android:launchMode="singleTask">

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="app.example.com" />
    </intent-filter>

    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="open" />
    </intent-filter>
</activity>

Server-side https://app.example.com/.well-known/assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": ["AA:BB:CC:...:99"]
    }
  }
]

I hit a very typical pitfall with fingerprints: the local debug package wouldn't verify. Finally discovered that the assetlinks.json contained the fingerprint of the upload key. With Play App Signing, you need to use the App signing key from Play Console, not the upload key; the fingerprint must be uppercase, colon-separated, and if you want to test with a debug package, add another entry for the debug keystore. Also, the file must be served over HTTPS without any redirects (http→https, example.com→www.example.com all fail verification), Content-Type must be application/json, and even with a trailing dot like example.com. it should return the same content. On Android 11 and below, it's even more harsh: a single host verification failure can pull down all App Links.

On Android 12+, each host is verified independently. On 15+, dynamic_app_link_components is supported, and on 17+ you can use am start --debug-link to see the matching process. But for daily troubleshooting, these three lines are most direct:

adb shell pm set-app-links --package com.example.app 0 all
adb shell pm verify-app-links --re-verify com.example.app
adb shell pm get-app-links com.example.app
# 看到 example.com: verified 才算过

Single Router So Parameters Don't Get Lost

Parsing, trust validation, gating, and navigation—this order can't change. Validation first to prevent Intent redirects and parameter injection; the gate must be able to "hold" the route and not discard it when blocked.

// 能去哪,收敛成一个有限集合
sealed interface Route {
    data class TaskDetail(val id: String, val from: String?) : Route
    data object Monitor : Route
    data object Home : Route
}

object DeepLinkParser {
    private val HTTPS_HOSTS = setOf("app.example.com", "m.example.com")

    fun parse(uri: Uri): Route? {
        val host = uri.host ?: return null
        when (uri.scheme) {
            "https" -> if (host !in HTTPS_HOSTS) return null
            "myapp" -> if (host != "open") return null
            else -> return null
        }
        val seg = uri.pathSegments
        return when {
            seg.getOrNull(0) == "task" && seg.size >= 2 && seg[1].length <= 64 ->
                Route.TaskDetail(seg[1], uri.getQueryParameter("from"))
            seg.getOrNull(0) == "monitor" -> Route.Monitor
            else -> null   // 不认识的一律丢掉,不做兜底跳转
        }
    }
}
class LaunchRouter(
    private val session: SessionProvider,
    private val pendingRoute: PendingRouteStore,
    private val navigator: Navigator,
    private val logger: ResidentLogger,
) {
    fun dispatch(activity: FragmentActivity, intent: Intent): Boolean {
        val data = intent.data ?: return false
        val route = DeepLinkParser.parse(data)
        if (route == null) {
            logger.warn("drop_untrusted_link", mapOf("uri" to data.toString()))
            return false
        }

        // 没过闸门就先存起来,授权完再重放
        val blocker = gateOf(route)
        if (blocker != null) {
            pendingRoute.save(route)
            navigator.openGate(blocker)
            return true
        }
        navigator.open(route, buildBackStack = activity.isTaskRoot)
        logger.event("launch", mapOf("route" to route::class.simpleName.orEmpty(),
                                     "source" to (data.getQueryParameter("from") ?: "unknown")))
        return true
    }

    private fun gateOf(route: Route): Gate? = when {
        !session.isPrivacyAgreed -> Gate.Privacy
        !session.isLoggedIn -> Gate.Login
        route is Route.Monitor && !session.hasLocationPermission -> Gate.PermissionPurpose
        else -> null
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        router.dispatch(this, intent)        // 冷 / 温启动
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        setIntent(intent)                    // 不写这行,下次取到的还是旧 Intent
        router.dispatch(this, intent)        // 热启动复用
    }
}

Back Button to Desktop Is the Watershed of Launch Quality

On cold start from an external link, the system only creates the target Activity by default, so pressing back goes straight to desktop. You need TaskStackBuilder to build the parent stack "Home → Detail", along with android:parentActivityName in the Manifest:

fun openTaskDetail(activity: Activity, taskId: String, buildBackStack: Boolean) {
    val detail = Intent(activity, TaskDetailActivity::class.java).apply {
        putExtra(EXTRA_TASK_ID, taskId)
    }
    if (!buildBackStack) {
        activity.startActivity(detail)
        return
    }
    TaskStackBuilder.create(activity).apply {
        addNextIntentWithParentStack(Intent(activity, MainActivity::class.java))
        addNextIntentWithParentStack(detail)
    }.startActivities()
}

For launchMode, I ended up with singleTask. standard piles up multiple detail pages if you click two links; singleTop only reuses at the top, creates new if there's already an instance; singleInstance splits the task stack, and two of yourself can appear in recents, so exclude it. However, singleTask has clear-stack semantics, so the handling in onNewIntent can't be skipped. Also, don't randomly change taskAffinity, or behavior in "Recents" will be weird.

Background Wants User to See Something, Only via Notification

Since Android 10, background startActivity is basically prohibited, so the correct approach is to put the wake-up action into PendingIntent and wait for the user to tap the notification.

private const val CHANNEL_ID = "resident"
private const val NOTIFICATION_ID = 1001
private const val REQ_OPEN = 2001
private const val REQ_STOP = 2002

fun buildResidentNotification(context: Context, state: ResidentState): Notification {
    val open = Intent(context, MainActivity::class.java).apply {
        action = Intent.ACTION_VIEW
        data = Uri.parse("https://app.example.com/monitor?from=notification")
        flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
    }
    val openPi = TaskStackBuilder.create(context).run {
        addNextIntentWithParentStack(open)
        getPendingIntent(
            REQ_OPEN,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    // 给用户一个明确的「停止」出口,这条不能省
    val stop = Intent(context, ResidentService::class.java).apply { action = ACTION_STOP }
    val stopPi = PendingIntent.getService(
        context, REQ_STOP, stop,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    return NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_stat_monitor)
        .setContentTitle("数据同步正在运行")
        .setContentText("已持续 ${state.minutes} 分钟 · 点此查看")
        .setContentIntent(openPi)
        .addAction(R.drawable.ic_stop, "停止", stopPi)
        .setOngoing(true)
        .setOnlyAlertOnce(true)
        .setCategory(NotificationCompat.CATEGORY_SERVICE)
        .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
        .build()
}

Per-version notes: 8.0 requires notification channel creation before startForeground; 12's PendingIntent throws IllegalArgumentException if mutability flag missing; 13's POST_NOTIFICATIONS means notification invisible if denied, need fallback; 14 to use PendingIntent.send() with background activity start permission, explicitly ActivityOptions.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED), and for cross-process bindService, use BIND_ALLOW_ACTIVITY_STARTS; 14 also requires dynamic registered broadcast receivers to be marked RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED; 16 strengthened Intent redirect protection, so all external Uri must go through whitelist.

Boot Broadcast Only for Rescheduling

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".trigger.BootReceiver"
    android:exported="true"
    android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
        // 只把兜底调度重新挂上,不在这里启服务、不开页面、不跑长任务
        RecoveryCoordinator.scheduleWatchdog(context.applicationContext)
    }
}

Restraint is reasonable. After 8.0, most implicit broadcasts are invalid for static registration; BOOT_COMPLETED is one of the few whitelisted ones. On 15, starting FGS types like dataSync, mediaProcessing, camera, microphone from boot broadcast is prohibited. Domestic ROMs' "auto-start management" is off by default, so whether broadcast arrives is uncertain; onReceive execution window is very short, a bit more work causes ANR. So in this architecture, boot event is positioned to reschedule watchdogs; continuation is left to other paths.

Testing commands for launch layer:

# 冷启动
adb shell am force-stop com.example.app
adb shell am start -W -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE -d "https://app.example.com/task/42?from=sms"

# 不 force-stop 再发一次,看 onNewIntent 有没有丢参数、有没有重复压页
adb shell am start -W -a android.intent.action.VIEW \
  -c android.intent.category.BROWSABLE -d "https://app.example.com/task/99?from=push"

# 恶意参数应该被静默丢掉,不打开任何页面
adb shell am start -W -a android.intent.action.VIEW -d "https://evil.example.com/task/1"

adb shell dumpsys activity activities | grep -i "com.example.app"
adb shell dumpsys notification | grep -i "com.example.app"

Persistence: How Long Can a Foreground Service Last

Accept "It Can Die at Any Time"

Mechanism What it does Impact on me
Process priority / oom_adj Foreground > visible > service > cached; LMK kills from bottom when memory tight startForeground just raises priority, not a lock
Doze / App Standby Buckets Limit network, Job, Alarm when still and screen off; bucket by usage frequency Polling and fixed heartbeat unreliable, must accept delay
8.0 background execution limits Background can't startService arbitrarily Use startForegroundService and must startForeground within 5s or ForegroundServiceDidNotStartInTimeException
12 background start FGS restriction Can't start FGS when app in background ForegroundServiceStartNotAllowedException; only via user action, notification interaction, or few exemptions
14/15/16 types and quotas Type mandatory, dataSync time limit, job quotas Detailed below
Domestic ROM power-saving Auto-start management, background power limits, freezing Same code: Pixel can last 10+ hours, some ROMs die in half hour

Once you realize this, the goal changes from "don't get killed" to "return after death." Chain C exists for that.

Permissions and Types: Manifest and Code Must Match

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application ...>
    <service
        android:name=".resident.ResidentService"
        android:exported="false"
        android:foregroundServiceType="dataSync|location" />
</application>

exported="false" is the default; this service should only start itself. Type written as dataSync|location, the code's typeMask must be identical, otherwise on 14+ it throws SecurityException—the root of the pitfall mentioned at the beginning.

Type selection table:

type Manifest permission Scenario Runtime prerequisite Restriction
dataSync FOREGROUND_SERVICE_DATA_SYNC Upload, sync, backup — Daily cumulative 6h since 15
mediaProcessing FOREGROUND_SERVICE_MEDIA_PROCESSING Media transcoding (new in 15) — Same 6h
location FOREGROUND_SERVICE_LOCATION Navigation, tracking Location permission + while-in-use condition Background start restricted
health FOREGROUND_SERVICE_HEALTH Health/sports monitoring ACTIVITY_RECOGNITION etc. While-in-use restriction
camera / microphone Corresponding type permissions Recording, calls CAMERA / RECORD_AUDIO Not enabled in background
mediaPlayback FOREGROUND_SERVICE_MEDIA_PLAYBACK Music, podcasts — —
phoneCall FOREGROUND_SERVICE_PHONE_CALL VoIP MANAGE_OWN_CALLS or dialer role —
connectedDevice FOREGROUND_SERVICE_CONNECTED_DEVICE Bluetooth / wearables Bluetooth permission —
remoteMessaging FOREGROUND_SERVICE_REMOTE_MESSAGING Cross-device message sync — —
shortService No extra type permission Short uninterruptible task ~3 min, can't combine with other types Must stop itself on time
specialUse FOREGROUND_SERVICE_SPECIAL_USE No matching type — Play requires purpose declaration, abuse rejected
mediaProjection FOREGROUND_SERVICE_MEDIA_PROJECTION Screen recording / casting 14+ re-authorize each session Can't cache authorization Intent
systemExempted FOREGROUND_SERVICE_SYSTEM_EXEMPTED System / privileged Requires system exemption Regular apps can't get

Only declare types truly needed. Adding more for "stability" complicates permission review, Play declarations, runtime checks; specialUse especially avoid.

The Service Itself

class ResidentService : Service() {

    private lateinit var stateStore: StateStore
    private lateinit var dispatcher: TaskDispatcher

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()          // 要早于 startForeground
        stateStore = StateStore(applicationContext)
        dispatcher = TaskDispatcher(stateStore)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_STOP -> {
                stateStore.transition(State.IDLE)
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
                return START_NOT_STICKY        // 用户主动停的,别让系统重建
            }
            ACTION_PAUSE -> stateStore.transition(State.PAUSED)
            ACTION_RESUME -> stateStore.transition(State.RECOVERING)
            else -> stateStore.transition(State.RUNNING)
        }

        val typeMask = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC or
                       ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
        ServiceCompat.startForeground(
            this, NOTIFICATION_ID,
            buildResidentNotification(this, stateStore.current()),
            typeMask
        )

        dispatcher.resumePendingWork()       // 从落盘队列断点续跑
        stateStore.transition(State.RUNNING)
        return START_STICKY
    }

    // Android 15 (API 35)+: dataSync / mediaProcessing run full daily 6h
    @RequiresApi(35)
    override fun onTimeout(startId: Int, fgsType: Int) {
        dispatcher.checkpoint()              // 先落盘,再停
        stateStore.transition(State.PAUSED)
        stopForeground(STOP_FOREGROUND_REMOVE)
        stopSelf()
    }

    override fun onTaskRemoved(rootIntent: Intent?) {
        // 只记一笔。在这里 startService 基本会被 ROM 拦,还容易被判异常唤醒
        stateStore.markTaskRemoved()
        super.onTaskRemoved(rootIntent)
    }

    override fun onDestroy() {
        dispatcher.checkpoint()
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

START_STICKY deserves more explanation; I was misled by it. Its semantics: after service is killed, the system attempts to recreate when resources allow, and calls onStartCommand with intent == null. It doesn't guarantee immediate recreation, nor the original Intent, and on most domestic ROMs it's blocked by auto-start policies. So it's the weakest of four recovery paths; relying on it alone will cause trouble.

onTimeout is added after upgrading to targetSdk 35. The system gives a very short processing window; not calling stopForeground + stopSelf quickly leads to ANR or forced kill, so order is first checkpoint to persist breakpoint, then tear down.

Start point also matters:

// 页面里,用户点了「开始」之后
fun startResident(context: Context) {
    val intent = Intent(context, ResidentService::class.java).apply { action = ACTION_START }
    ContextCompat.startForegroundService(context, intent)
}

On Android 12+, starting a foreground service in background is denied. Common exemptions: user-visible Activity interaction, notification click or bubble interaction, SYSTEM_ALERT_WINDOW with visible overlay (tightened in 15 to require actual overlay), exact alarms, geofence or activity recognition callbacks, and system roles like dialer. Don't gamble outside exemptions; place the call on the "user just clicked a button" path.

What Really Determines Success Is How State Is Stored

data class ResidentSnapshot(
    val state: State,
    val sessionId: String,             // 一次任务一个 ID,用来去重
    val queue: List<WorkItem>,         // 未完成任务,每条带 dedupeKey
    val lastProgress: Long,            // 断点位置
    val updatedAt: Long,
    val userAuthorizedUntil: Long,     // 授权窗口,过期就不再自动恢复
)

class StateStore(context: Context) {
    private val ds = context.getSharedPreferences("resident", MODE_PRIVATE)

    fun checkpoint(snapshot: ResidentSnapshot) {
        ds.edit().putString(KEY, Json.encodeToString(snapshot)).apply()
    }
    fun current(): ResidentSnapshot = /* 反序列化;坏了就降级成 IDLE,别抛异常 */
    fun transition(state: State) = checkpoint(current().copy(state = state, updatedAt = now()))
}
class TaskDispatcher(private val store: StateStore) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val inFlight = Collections.synchronizedSet(mutableSetOf<String>())

    fun resumePendingWork() {
        val snapshot = store.current()
        if (snapshot.state == State.IDLE) return
        if (System.currentTimeMillis() > snapshot.userAuthorizedUntil) {
            store.transition(State.IDLE); return          // 授权过期,不偷偷复活
        }
        snapshot.queue.forEach { item ->
            if (!inFlight.add(item.dedupeKey)) return@forEach   // 同一个 key 只跑一次
            scope.launch {
                try {
                    uploadIncremental(item, snapshot.lastProgress)
                    store.remove(item.dedupeKey)
                } catch (e: Exception) {
                    store.markFailed(item.dedupeKey)     // 留在队列等下次,别清空
                } finally {
                    inFlight.remove(item.dedupeKey)
                }
            }
        }
    }

    fun checkpoint() = store.transition(State.PAUSED)
}

A few lessons bought with incidents: checkpoint whenever progress advances, don't wait for onDestroy which may not be called; each task carries dedupeKey to deduplicate on recovery; validate authorization window before recovery, if user stopped or expired, no auto-revival; service layer shouldn't hold Activity or View, only deal with StateStore and Repository.

Scheduling: Don't Write Your Own Loop

Need Use Pitfall
Delayed OK, needs retry & constraints (network, charging) WorkManager Min period 15 min, not real-time heartbeat
Finer system scheduling, need quota awareness JobScheduler 16+ jobs from FGS also subject to quota
Exact moment (alarm / schedule) AlarmManager.setExactAndAllowWhileIdle 12+ needs SCHEDULE_EXACT_ALARM; 13+ USE_EXACT_ALARM only for alarm/clock apps, Play policy
User-initiated large data transfer user-initiated data transfer job (API 34+ setUserInitiated(true) + RUN_USER_INITIATED_JOBS) User-visible, cancellable; 16+ exempt from normal job quota
Immediate messaging Vendor push / FCM high-priority data message System pulls process, more reliable than self-owned long connection
class ResidentWatchdogWorker(appContext: Context, params: WorkerParameters)
    : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        val store = StateStore(applicationContext)
        val s = store.current()
        if (s.state == State.IDLE || now() > s.userAuthorizedUntil) return Result.success()
        return try {
            RecoveryCoordinator.resumeIfNeeded(applicationContext)
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

fun scheduleWatchdog(context: Context) {
    val req = PeriodicWorkRequestBuilder<ResidentWatchdogWorker>(15, TimeUnit.MINUTES)
        .setConstraints(
            Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
        )
        .build()
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "resident_watchdog", ExistingPeriodicWorkPolicy.KEEP, req
    )
}

The watchdog only "checks state, resumes from breakpoint if needed", not a second-level heartbeat. Handler.postDelayed loops, while(true) with Thread.sleep in service can't withstand Doze and are easily flagged as abnormal wake-ups. For long connections, place under FGS, adaptive backoff for heartbeat based on network type and Doze state, exponential backoff and jitter for reconnection, persist undelivered messages.

The Last Mile of Domestic ROMs

If user hasn't whitelisted app for auto-start/background, even compliant code may be frozen. But guidance must be compliant: explain first, then jump to settings, and allow denial.

fun showBackgroundPurposeDialog(activity: Activity, onAccept: () -> Unit) {
    // 原生 AlertDialog,没引弹窗库;文案要说清用途、范围、能随时关
    AlertDialog.Builder(activity)
        .setTitle("需要允许后台运行")
        .setCancelable(true)
        .setMessage(
            "为保证您在锁屏或切换到其他应用后,仍能持续收到数据同步结果与提醒," +
            "需要允许本应用在后台运行。\n\n" +
            "· 仅在您开启同步期间使用\n" +
            "· 不会额外读取与同步无关的信息\n" +
            "· 可随时在「设置 → 应用 → 电池」中关闭,也可在通知栏点「停止」"
        )
        .setNegativeButton("暂不开启", null)
        .setPositiveButton("去设置") { _, _ -> onAccept() }
        .show()
}

private fun openBatterySettings(activity: Activity) {
    val pkg = activity.packageName
    val chain = listOf(
        Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
            .setData(Uri.parse("package:$pkg")),
        Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS),
        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
            .setData(Uri.parse("package:$pkg"))
    )
    for (i in chain) {
        try {
            activity.startActivity(i)
            return
        } catch (e: ActivityNotFoundException) {
            // 各家 ROM 支持度不一样,逐级降级,别让设置页打不开就崩
        }
    }
}

ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS has clear use cases on Google Play; if not applicable, guide user to system settings, don't silently request. There's no unified public Intent for vendor auto-start management; use ACTION_APPLICATION_DETAILS_SETTINGS fallback plus instructions. Record denial, don't repeatedly pop dialogs; functional degradation to "catch up next time user opens app" rather than sneaky retries. Review copy with privacy policy and permission list.

Four recovery paths:

Trigger Reliability What it does
START_STICKY Low, ROM blocks often Best-effort rebuild, read state when intent==null
WorkManager watchdog Medium-high, may be delayed Fallback check + resume, min 15 min
Vendor push / FCM High, system pulls process Resume on message arrival, most "legal" wake-up source
User reopens app Highest Call resumeIfNeeded() in Application.onCreate or home

All four converge to RecoveryCoordinator, with sessionId and dedupeKey ensuring only one execution even if multiple sources arrive.

The "Keep-Alive" Schemes I Deleted

Practice Status now Why not
Dual-process guardian / AIDL mutual pull Basically invalid since 8.0+, ROM blocks No effect, battery drain, association start
Native fork child process Cleaned by lmkd / cgroup Compatibility disaster, security risk
1-pixel Activity Detected by anti-shake and background start limits No effect, intrusive
Play silent audio Detected by system and app store Clearly violating
High-frequency system broadcast wake Implicit broadcasts heavily restricted Limited effect, flagged as abnormal wake
Mutual app wake / ContentProvider pull Vendor crackdown Compliance risk
Abused account sync / accessibility / notification listener Sensitive permissions, store scrutiny Permission not matching purpose, violates minimal necessary

Payoff is just "a few more minutes on some devices", cost is battery list, bad reviews, rejection, report; negative ROI.

Version Limits Checked One by One

This table goes into my project README and Trae's project rules—AI-generated code most easily misses version branches.

Version Limit added Impact Code correspondence
6.0 (23) Doze / App Standby Polling, heartbeat unreliable Event-driven, accept delay
7.0 (24) Removed some implicit broadcasts Network change etc. wake lost ConnectivityManager.NetworkCallback / WorkManager
8.0 (26) Background can't startService; notification channels Must startForegroundService and startForeground within 5s ContextCompat, NotificationChannel
9.0 (28) Introduced FOREGROUND_SERVICE permission Without declaration SecurityException Manifest
10 (29) Background start Activity restriction; FGS needs location type Background goes through notification + PendingIntent Notification section
11 (30) Package visibility <queries>; FGS needs camera/microphone types Declare before explicit Intent to other app Manifest
12 (31) Background FGS start prohibited; PendingIntent mutability; explicit exported Start point in user interaction; add FLAG_IMMUTABLE Service start section
13 (33) POST_NOTIFICATIONS runtime permission Notification invisible if denied, fallback Show purpose first then request
14 (34) FGS type mandatory + type permission; background Activity start tightened; dynamic broadcast export flag; MediaProjection re-authorize each session typeMask and Manifest must match Manifest + ServiceCompat
15 (35) dataSync / mediaProcessing daily 6h, onTimeout; BOOT_COMPLETED can't start some FGS; SYSTEM_ALERT_WINDOW exemption tightened Long tasks segment + breakpoint; boot only schedule onTimeout / BootReceiver
16 (36) Jobs from FGS also subject to JobScheduler quota; Intent redirect hardening Use user-initiated job exemption; external Intent all whitelist Scheduling section / DeepLinkParser
17+ Background audio needs while-in-use type and can't be shortService; --debug-link diagnostics Call recording type re-validation Follow official Behavior Changes

Building This with Trae: The Prompts I Actually Sent

This code was produced through many rounds of conversation with Trae. At first, being lazy, I said "help me write a background resident service", and it gave me a dual-process AIDL mutual pull, the 2016 style, didn't even compile. Then I changed to setting rules first, one thing per round, and each round it gave acceptance commands; it went smoothly.

Pattern: from scratch create directories and multi-file linkage via Builder (Ctrl+I, Cmd+I on Mac); single-file refinement, explanation and debugging via Chat (Ctrl+Shift+A); refactoring selection with Ctrl+K; SOLO only for "add unit tests" self-contained tasks, never for state machine and permission flows. For architecture and compliance rounds, switch to stronger reasoning model; for batch file creation, use fast one. Context via #File / #Folder precisely pointing to AndroidManifest.xml, resident/ etc., don't expect full-repo auto-indexing—project big, indexing often incomplete.

First Pin the Rules

Two files in root. .traeignore to block index noise:

.gradle/
.idea/
build/
app/build/
*/build/
local.properties
*.apk
*.aab
*.keystore
captures/
.externalNativeBuild/
.cxx/

Another is .trae/rules/project_rules.md (may be "custom rules" or "AI rules" depending on version). Content:

# 项目规则 / Android 唤起与常驻架构

## 技术栈
- Kotlin,单 Activity,minSdk 26,targetSdk 35,同时兼容 Android 16(API 36) 行为。
- 只允许使用 AndroidX / Jetpack 官方组件(core-ktx、appcompat、work-runtime-ktx)。
- 禁止引入任何第三方库、第三方保活 SDK、第三方路由框架、第三方推送 SDK。
- 权限用途说明与设置引导一律使用原生 AlertDialog,不引第三方弹窗库。

## 合规红线(最高优先级)
- 禁止生成双进程守护、1 像素 Activity、无声音频、Native fork、应用间互相拉起、
  ContentProvider 拉活、高频广播拉活、账号同步/无障碍滥用等任何绕过系统后台限制的代码。
- 后台持续任务必须是用户可感知的前台服务,通知必须提供显式「停止」操作。
- 任何权限申请前必须先展示用途说明,用户可拒绝,拒绝后不得反复弹窗。
- 进程恢复必须校验用户授权窗口;用户主动停止后不得自动复活。

## 编码约定
- 每一个依赖系统版本的行为都要用 Build.VERSION.SDK_INT 分支,并在注释里标注 API level。
- Manifest 中带 intent-filter 的组件必须显式声明 android:exported。
- 前台服务的 android:foregroundServiceType 与 startForeground 的 typeMask 必须完全一致。
- 所有外部传入的 Intent / Uri 必须经 DeepLinkParser 白名单校验,未知路径静默丢弃。
- 状态必须落盘,任务必须带 dedupeKey 保证幂等。
- 类文件顶部写一行职责说明;不要生成 TODO 占位实现。

## 输出要求
- 先给方案(分层、文件清单、版本差异),我确认后再写代码。
- 每次只完成本轮约定的文件,不要顺手重构其他模块。
- 每个文件给出完整路径,并列出本轮的验收用 adb 命令。
- 不确定的系统行为,标注「需查官方文档」而不是编造 API。

This step is most efficient. Writing constraints once saves ten rounds of repetition and prevents mixing in old code. After creating, I sent a confirmation in Chat:

请读取项目根目录下的 .traeignore 和 .trae/rules/project_rules.md,
用一段话复述你理解的项目技术栈、依赖限制、合规红线和输出要求,
并明确列出你在本项目中「绝对不会生成」的 5 类代码。
本轮不要创建或修改任何源码文件。

It missed one compliance red line; I replied: "Point 3 not repeated, re-read rules file, future output must treat rules as highest priority." This read-back seems redundant but saves rework.

First Round: Only Plan, No Code

Using strong reasoning model, #Folder pointing to empty app/src/main, send:

你是 Android 平台工程师。我们要为一个 minSdk 26 / targetSdk 35、Kotlin、仅使用 AndroidX 的
单 Activity 项目,设计「应用唤起 + 后台常驻」架构,业务场景是:用户可从 H5 链接、
通知、厂商推送打开详情页,并可主动开启一段后台持续上报任务。

本轮【禁止写任何代码】,只输出方案:

1. 分层设计:触发层 / 唤起层 / 常驻层 / 恢复层 / 可观测层,逐层说明职责与禁止事项;
2. 完整文件树(给出每个文件的完整路径与一句话职责);
3. 三条关键链路的文字时序:外部唤起冷启动、用户开启常驻、进程被杀后恢复;
4. 状态机:列出状态、事件、以及每次状态转换需要持久化的字段;
5. 版本差异清单:逐条列出 Android 8/10/12/13/14/15/16 对「后台启动 Activity、
   后台启动前台服务、前台服务类型与配额、通知权限、PendingIntent」的限制,
   以及每一条限制对应到哪个文件里的哪处分支;
6. 合规红线:列出明确不会采用的做法及原因;
7. 验收方案:列出需要覆盖的 8 个手工测试场景和对应 adb 命令。

输出用 Markdown 表格与编号列表,不要代码块。对不确定的系统行为标注「需查官方文档」。

First plan missed onTimeout and warm-start parameters, I chased:

方案里缺少三点,请补充后再定稿:
(1) Android 15 对 dataSync 类型前台服务的时长上限与超时回调如何处理;
(2) 进程被杀后除了 START_STICKY 还有哪三条恢复触发源,如何避免重复执行;
(3) 热启动(singleTask 复用)时链接参数不丢的具体机制。

Skipping planning and letting Builder generate all at once failed: files referenced inconsistently, permissions and types misaligned, cleanup took longer. Plan first, then fill skeleton.

Manifest, Permissions, and Version Constants

In Builder, #File with confirmed plan:

按已确认的架构方案,本轮只完成「清单与版本适配骨架」,涉及文件:
- app/src/main/AndroidManifest.xml
- app/src/main/java/com/example/app/platform/AndroidVersions.kt
- app/src/main/java/com/example/app/resident/ResidentContract.kt

要求:
1. Manifest 声明以下权限并逐条写注释说明用途:INTERNET、FOREGROUND_SERVICE、
   FOREGROUND_SERVICE_DATA_SYNC、FOREGROUND_SERVICE_LOCATION、POST_NOTIFICATIONS、
   ACCESS_FINE_LOCATION、RECEIVE_BOOT_COMPLETED、WAKE_LOCK;
2. 声明 MainActivity(android:exported=true、launchMode=singleTask),包含三组 intent-filter:
   LAUNCHER、autoVerify=true 的 https App Links(host=app.example.com)、
   单独的自定义 scheme(myapp://open)——后两组必须分开,不得混写;
3. 声明 ResidentService:android:exported=false、
   android:foregroundServiceType="dataSync|location";
4. 声明 BootReceiver:exported=true 且只接受 BOOT_COMPLETED,显式设置 android:permission;
5. 所有带 intent-filter 的组件显式写 android:exported;
6. AndroidVersions.kt 提供语义化判断(如 supportsFgsTypeRequirement() 对应 API 34、
   supportsFgsTimeout() 对应 API 35、supportsUserInitiatedJob() 对应 API 34),
   每个函数注释标注 API level;
7. ResidentContract.kt 集中定义 ACTION_START / ACTION_STOP / ACTION_PAUSE / ACTION_RESUME、
   NOTIFICATION_ID、CHANNEL_ID、请求码常量,禁止在其他文件里散落字符串字面量;
8. 额外输出一份 assetlinks.json 模板(放到 docs/assetlinks.example.json),
   指纹处用占位符并注明「必须替换为发布签名证书的 SHA-256,大写、冒号分隔」。

硬约束:不引入任何新依赖;不生成任何保活黑科技;不确定的权限名标注「需查官方文档」。
输出先列文件清单与关键决策,再给完整代码,最后给 3 条 adb 验收命令。

It wrote https and myapp into same filter, service type only dataSync; I pointed out and asked to fix:

你把 https 和 myapp 写进了同一个 intent-filter,这会产生意外的匹配组合;
另外 ResidentService 的 foregroundServiceType 写成了 dataSync 单类型,
与方案约定的 dataSync|location 不一致。请按方案修正,并说明 Manifest 类型
与 startForeground 的 typeMask 一致性在 Android 14+ 为什么是强制的。

After install, run adb shell dumpsys package com.example.app | grep -i -A2 "foregroundServiceType" and adb shell pm get-app-links.

Launch Layer

Builder, #File with Manifest and ResidentContract.kt:

本轮只实现「唤起层」,涉及文件:
- app/src/main/java/com/example/app/launch/Route.kt
- app/src/main/java/com/example/app/launch/DeepLinkParser.kt
- app/src/main/java/com/example/app/launch/GateKeeper.kt
- app/src/main/java/com/example/app/launch/PendingRouteStore.kt
- app/src/main/java/com/example/app/launch/Navigator.kt
- app/src/main/java/com/example/app/launch/LaunchRouter.kt
- app/src/main/java/com/example/app/ui/MainActivity.kt

要求:
1. Route 用 sealed interface 收敛为 TaskDetail(id, from) / Monitor / Home 三种;
2. DeepLinkParser 对 scheme 与 host 做白名单:https 仅允许 app.example.com、m.example.com,
   自定义 scheme 仅允许 myapp://open;路径仅识别 /task/{id} 与 /monitor;
   id 长度限制 64 且做字符校验;未知路径返回 null,禁止兜底跳转;
   解析失败不得把原始 Uri 拼进异常或日志以外的任何地方;
3. LaunchRouter.dispatch(activity, intent) 是唯一分发入口,顺序固定为:
   解析 → 可信校验 → 闸门(隐私同意 / 登录 / 权限用途说明) → 导航 → 埋点;
   闸门未通过时用 PendingRouteStore 把 Route 持久化,授权完成后重放,禁止丢弃参数;
4. MainActivity 的 onCreate 与 onNewIntent 都调用同一个 dispatch;
   onNewIntent 必须 setIntent(intent),注释写明「不写会丢热启动参数」;
5. Navigator 在 activity.isTaskRoot 时用 TaskStackBuilder 合成 首页→详情 返回栈,
   非 taskRoot 时直接打开,避免重复压栈;
6. 所有外部 Uri 参数在埋点前脱敏,只记录 route 类型与 from 来源,不记录完整 URL 与用户参数;
7. 使用原生 API,不引入 ARouter 等任何路由框架;不写 TODO 占位实现。

输出:先给「冷启动 / 温启动 / 热启动」三种场景下的参数流转说明,再给完整代码,
最后给 4 条 adb 验收命令(含 force-stop 后的冷启动与不 force-stop 的热启动)。

First version returned false for not logged in, losing route; Navigator unconditional parent stack, hot start repeats. Follow-up:

你在 LaunchRouter 里直接对未登录情况 return false 丢弃了路由,违反「闸门必须暂存」约定;
另外 Navigator 无条件 addNextIntentWithParentStack 会导致热启动重复压栈。
请按「pendingRoute 持久化 + isTaskRoot 判断」修正,并补充 PendingRouteStore 的重放入口
应该由哪个回调触发。

Persistence Layer

Builder, #File with Manifest, ResidentContract.kt, AndroidVersions.kt:

本轮只实现「常驻层」,涉及文件:
- app/src/main/java/com/example/app/resident/ResidentService.kt
- app/src/main/java/com/example/app/resident/NotificationFactory.kt
- app/src/main/java/com/example/app/resident/ResidentEntry.kt

要求:
1. ResidentService 继承 Service,onBind 返回 null;onCreate 里先创建通知渠道;
2. onStartCommand 处理 ACTION_START / ACTION_RESUME / ACTION_PAUSE / ACTION_STOP:
   - ACTION_STOP:状态置 IDLE、stopForeground(STOP_FOREGROUND_REMOVE)、stopSelf、
     返回 START_NOT_STICKY(用户主动停止不允许复活);
   - 其余分支调用 androidx.core.app.ServiceCompat.startForeground,
     typeMask 必须严格等于 Manifest 里的 dataSync|location,用注释写明两者的对应关系;
   - 正常情况返回 START_STICKY,并用注释说明它「不保证立即重建、intent 可能为 null、
     国产 ROM 可能拦截」,因此不能作为唯一恢复手段;
3. 覆盖 Android 15(API 35)+ 的 onTimeout(startId, fgsType):先 checkpoint 落盘断点,
   再 stopForeground + stopSelf;注释说明 dataSync 每日累计 6 小时上限与超时不处理的后果;
4. onTaskRemoved 只做状态标记,禁止在其中 startService;onDestroy 必须 checkpoint;
5. Android 8(26)+ 使用 ContextCompat.startForegroundService 启动,并在注释中写明
   「必须在 5 秒内 startForeground」以及「Android 12+ 后台调用会被拒,启动点必须在用户交互内」;
6. NotificationFactory 构建常驻通知:
   - 通知渠道 IMPORTANCE_LOW,setOnlyAlertOnce(true)、setOngoing(true)、CATEGORY_SERVICE;
   - 内容点击 PendingIntent 走 App Links Uri 到 /monitor?from=notification,
     用 TaskStackBuilder 合成父栈,FLAG_IMMUTABLE + FLAG_UPDATE_CURRENT;
   - 必须提供一个「停止」Action,PendingIntent.getService 指向 ACTION_STOP;
   - 文案写明正在做什么、已持续多久、可随时停止;
7. 禁止:无界循环、Handler 死循环心跳、持有 Activity 引用、任何保活黑科技、任何新依赖。

输出:先给生命周期时序说明(含被杀场景),再给完整代码,最后给 5 条 adb 验收命令
(含 dumpsys activity services 与通知渠道检查)。

It missed typeMask, onTimeout, and stop action in first version. Follow-up:

生成的代码存在三个问题,请逐个修正并说明原因:
(1) startForeground 没有传 typeMask,Android 14 会抛什么异常;
(2) 缺少 onTimeout,Android 15 上 dataSync 跑满 6 小时会发生什么;
(3) 通知没有「停止」出口,为什么这在合规上不可接受。
另外请把「后台启动前台服务在 Android 12+ 被拒」的规避方式写成注释,不要写成绕过代码。

State Machine and Idempotency: I Didn't Dare Hand to SOLO

Chat with strong reasoning, #Folder with resident/:

本轮实现「可恢复」的核心,涉及文件:
- app/src/main/java/com/example/app/state/State.kt
- app/src/main/java/com/example/app/state/ResidentSnapshot.kt
- app/src/main/java/com/example/app/state/StateStore.kt
- app/src/main/java/com/example/app/state/TaskDispatcher.kt
- app/src/main/java/com/example/app/state/RecoveryCoordinator.kt

要求:
1. State 枚举:IDLE / RUNNING / PAUSED / RECOVERING / STOPPED,
   给出合法转换表,非法转换记录日志但不崩溃;
2. ResidentSnapshot 字段固定为:state、sessionId、queue(List<WorkItem>,每项带 dedupeKey)、
   lastProgress、updatedAt、userAuthorizedUntil;用 SharedPreferences 的原生方式实现
   (不引入新依赖),反序列化失败时降级为 IDLE 而不是抛异常;
3. StateStore.transition() 每次转换都落盘;checkpoint() 在任何进度推进后调用;
4. TaskDispatcher.resumePendingWork() 必须幂等:
   - 用 dedupeKey 去重,同一个 key 不并发、不重复执行;
   - 失败时把任务留在队列并标记失败次数,不清空;
   - 使用 SupervisorJob + Dispatchers.IO,不阻塞主线程;
5. RecoveryCoordinator.resumeIfNeeded(context) 是「唯一恢复入口」,被以下四处共同调用:
   START_STICKY 重建、WorkManager 看门狗、推送到达、Application/首页打开;
   它必须先校验 userAuthorizedUntil(过期或 state==IDLE 直接返回,禁止自动复活),
   再启动服务并传 ACTION_RESUME;
6. 明确注释:为什么「进程随时会死」是本设计的前提,以及 onDestroy 不可靠时靠什么兜底;
7. 不要生成任何新依赖,不要生成 TODO。

输出:先给状态转换表与「多源同时到达」的并发时序说明,再给完整代码,
最后列出 6 个需要单元测试覆盖的用例(用文字描述即可,本轮不写测试)。

resumeIfNeeded lacked authorization window check, must chase:

resumeIfNeeded 缺少 userAuthorizedUntil 校验,会导致用户点「停止」后被 WorkManager 偷偷复活,
这是合规问题,请修正。另外请补充:当 START_STICKY、看门狗、推送三者在同一秒内同时到达时,
如何保证只启动一次恢复流程(给出具体去重机制,不要只写「加锁」两个字)。

Scheduling and Trigger

Builder, #File with RecoveryCoordinator.kt, AndroidVersions.kt:

本轮只实现「触发与调度」,涉及文件:
- app/src/main/java/com/example/app/trigger/BootReceiver.kt
- app/src/main/java/com/example/app/trigger/PushReceiver.kt
- app/src/main/java/com/example/app/trigger/ResidentWatchdogWorker.kt
- app/src/main/java/com/example/app/trigger/ScheduleRules.kt

要求:
1. BootReceiver 只做两件事:调用 ScheduleRules.scheduleWatchdog() 重新挂兜底调度、
   给 StateStore 标记一次重启事件;禁止在其中启动前台服务、跑耗时逻辑或直接开页面;
   注释写明 Android 15 起 BOOT_COMPLETED 不允许启动部分类型的前台服务;
2. PushReceiver 收到推送后只调用 RecoveryCoordinator.resumeIfNeeded(),
   并在注释里说明「推送由系统代拉进程,是最合法的唤醒源」;
   不得在广播里直接 startActivity(Android 10+ 后台启动 Activity 限制);
3. ResidentWatchdogWorker 用 PeriodicWorkRequestBuilder 最小周期 15 分钟,
   约束 NetworkType.CONNECTED,失败返回 Result.retry(),
   用 enqueueUniquePeriodicWork + KEEP 防止重复入队;
   注释说明「看门狗只做检查与断点续跑,不是秒级心跳」;
4. ScheduleRules 额外给出 Android 16(API 36) 分支说明:
   从前台服务里启动的 JobScheduler job 也受配额约束,
   用户主动触发的大数据传输应使用 user-initiated data transfer job(API 34+),
   这一部分只写清晰的注释与接入点,不要编造不存在的 API;
5. 精确闹钟相关:注释说明 SCHEDULE_EXACT_ALARM / USE_EXACT_ALARM 的适用版本与政策限制,
   本轮不实现精确闹钟;
6. 不引入任何新依赖(WorkManager 使用项目已有的 work-runtime-ktx)。

输出:先给「四条恢复触发源 → 唯一恢复入口」的收敛示意,再给完整代码,
最后给 4 条 adb 验收命令(含 dumpsys jobscheduler 与 am broadcast 模拟开机)。

Compliance Dialogs

This round's wording I sent to legal separately; all technically correct, but copy lacking scope, duration, closability gets corrected.

本轮实现「权限与后台运行的合规引导」,涉及文件:
- app/src/main/java/com/example/app/compliance/PermissionPurposeDialog.kt
- app/src/main/java/com/example/app/compliance/BatteryGuide.kt
- app/src/main/java/com/example/app/compliance/ConsentGate.kt

要求:
1. 一律使用原生 AlertDialog.Builder,禁止引入任何第三方弹窗/权限库;
2. 在申请 POST_NOTIFICATIONS、定位权限、以及跳转电池优化设置【之前】,
   先展示用途说明:说明收集什么、用于什么功能、仅在什么期间使用、用户可随时关闭;
   提供明确的「暂不开启」按钮,用户可拒绝;
3. ConsentGate 记录用户是否拒绝及拒绝次数:拒绝后不得在同一会话反复弹窗,
   功能降级为「用户下次主动打开 App 时补做」,禁止用循环或定时重试骚扰用户;
4. BatteryGuide 打开设置的 Intent 采用三级降级:
   ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS →
   ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS →
   ACTION_APPLICATION_DETAILS_SETTINGS,
   每级用 try/catch ActivityNotFoundException 兜底,绝不崩溃;
   注释说明该权限在 Google Play 的政策限制,以及不符合用例时应引导用户自行到系统设置开启;
5. 弹窗文案需与隐私政策、权限申请清单的表述保持一致,用常量集中管理便于法务 review;
6. 所有文案常量放在 res/values/strings.xml,代码中不写死中文字符串。

输出:先给「权限申请 → 用途说明 → 用户决策 → 降级」的流程图(Mermaid),
再给完整代码与 strings.xml 条目,最后给 5 条手工验收场景。

Testing and Metrics

Self-contained, I gave to SOLO:

本轮只做测试与埋点,不改动任何生产代码逻辑。

一、单元测试(src/test):
1. DeepLinkParser:合法 https / 合法 myapp / 非法 host / 非法 scheme /
   超长 id / 未知路径 / 空 Uri,共 7 个用例,断言非法输入一律返回 null;
2. StateStore:正常读写、反序列化损坏降级为 IDLE、transition 落盘;
3. TaskDispatcher:同一 dedupeKey 并发只执行一次、失败后任务仍留在队列;
4. RecoveryCoordinator:授权窗口过期不复活、state==IDLE 不复活、四源并发只触发一次恢复。

二、instrumentation / 手工场景(只输出操作步骤,不写自动化脚本):
冷启动链接唤起返回栈、热启动参数不丢、通知点击、用户点「停止」后不复活、
权限被拒后降级、进程被 force-stop 后由看门狗恢复。

三、可观测性:
新增 ResidentLogger.kt,定义结构化事件:launch(route, source)、
fgs_start(typeMask)、fgs_timeout(reason)、recovery(trigger, success, elapsedMs)、
consent(action, granted)、stop(source)。
要求:只记录事件类型与非个人标识的参数,禁止记录完整 URL、位置坐标、用户标识;
输出一份 Markdown 埋点字典表(事件名 / 参数 / 用途 / 是否含个人信息)。

不引入任何新依赖;不修改生产代码逻辑;不确定的断言标注「需人工确认」。

Final Round: Let It Self-Review

Chat, #Folder with whole app/src/main:

基于当前项目的全部源码,做一次架构复盘,只输出文档不修改代码:

1. 输出一张 Mermaid flowchart:五层架构 + 三条链路的数据流向,节点使用中文;
2. 输出一张 Mermaid sequenceDiagram:进程被系统回收后的恢复链路;
3. 逐条核对 Android 8/10/12/13/14/15/16 的版本差异清单,
   列出「已覆盖 / 未覆盖 / 需人工确认」三列;
4. 输出一份上架前检查表:权限声明与隐私政策一致性、前台服务类型用途说明、
   通知可停止、用户拒绝后的降级、电池优化引导文案、埋点是否含个人信息;
5. 输出一份「继续深入」清单:列出 8 个后续可优化方向,按投入产出比排序;
6. 最后把以上内容组织成一篇掘金技术文章的 Markdown 大纲(含每章要点与建议配图)。

对源码里没有实现的部分,明确标注「当前项目未实现」,不要编造。

In retrospect, three things worked: constraints written once into rules, not repeated each round; plan before code; each round specify "only which files" and "how to verify", including its own adb commands—branches it misses often appear when writing acceptance commands. The phrase "mark uncertain system behaviors as 'needs official docs'" blocks fabricated permission names. After Builder generates, don't whole-Accept; for service and state machine I read through Diff line by line.

When It Fails, How I Follow Up

Debugging follows a fixed four-part: exception stack, environment, already excluded, requirements. For compile errors, use AI entry next to terminal error to grab stack; runtime issues save adb logcat -v brief > logcat.txt, reference with #File in Chat.

【异常】完整粘贴 logcat 异常栈(不要删减 Caused by 部分)
【环境】设备型号 / ROM / Android 版本 / targetSdk / 是否开了电池优化白名单
【已排除】我已经确认 Manifest 声明了 X、权限 X 已授予、typeMask 与 Manifest 一致
【要求】
1. 先按可能性从高到低给出 3 个根因假设,并说明每个假设如何用一个 adb 命令证伪;
2. 再给出最小改动补丁,只改必要的文件,不要顺手重构;
3. 补丁里标注涉及的 API level,并说明在低版本设备上是否会引入兼容性问题;
4. 最后给一条能验证修复是否生效的 adb 命令。

Most common four questions:

Symptom How I ask
SecurityException: Starting FGS requires a valid type / MissingForegroundServiceTypeException Compare Manifest foregroundServiceType and ServiceCompat.startForeground typeMask bit by bit and list differences; check both FOREGROUND_SERVICE_* permissions; only fix inconsistencies
ForegroundServiceStartNotAllowedException Find all ContextCompat.startForegroundService call sites, mark which in user-visible interaction, which in BroadcastReceiver/Worker; change latter to send notification for user click, or let RecoveryCoordinator decide; no code bypassing background restrictions
App Links opens browser / verified: false Check assetlinks.json is HTTPS direct no redirect, Content-Type, fingerprint uppercase and release signing cert (Play App Signing use App signing key), autoVerify separate from custom scheme, then give full pm set-app-links / verify-app-links --re-verify / get-app-links sequence
Notification not showing / FGS dies instantly Check POST_NOTIFICATIONS(33+) granted, NotificationChannel created before startForeground, startForeground within 5 seconds, notification smallIcon and contentIntent valid, give dumpsys notification and logcat filter commands

What I Go Through Before Launch

Compliance first, easier to get stuck than tech:

Then the pitfall table:

Symptom Root cause Fix
SecurityException: Starting FGS requires a valid type 14+ missing type or type permission Add foregroundServiceType and corresponding FOREGROUND_SERVICE_*
while-in-use restriction SecurityException location/camera/microphone/health restricted in background Start when user-visible, or change type
ForegroundServiceStartNotAllowedException 12+ starting FGS in background Move start point into user interaction; background scenarios via notification click/recovery entry
ForegroundServiceDidNotStartInTimeException Didn't startForeground within 5s Move initialization out of critical path, startForeground first, then work
15 service suddenly stops dataSync/mediaProcessing daily 6-hour limit Implement onTimeout, segment task + schedule resume
Notification not showing POST_NOTIFICATIONS(33+) not granted / channel not created / icon invalid Add purpose explanation, channel first, smallIcon
PendingIntent throws IllegalArgumentException 12+ missing mutability flag Add FLAG_IMMUTABLE
INSTALL_PARSE_FAILED_MANIFEST_MALFORMED 12+ missing android:exported Add all
App Links open browser or chooser assetlinks.json verification failed Check release signing fingerprint, uppercase, no redirect, Content-Type, autoVerify
Hot start link parameters lost Only read intent.data in onCreate onNewIntent + setIntent() also call dispatch
Back button to desktop External launch didn't build parent stack TaskStackBuilder rebuild Home → Detail
Double tap two detail pages launchMode=standard and no reuse Converge to singleTask/singleTop + single Router
After swiping away task card, never comes back Treating onTaskRemoved restart as only method Multi-source recovery + state externalized
After user clicks "Stop", revived again WorkManager/START_STICKY didn't check stop state Unified recovery entry checks state/authorization window
Duplicate reporting after recovery No idempotency dedupe Each task has dedupeKey
Domestic ROM dies in minutes Auto-start/battery optimization whitelist not enabled Purpose explanation + settings guide + recoverable design
Background direct page fails 10+ background startActivity limit Send notification, user clicks PendingIntent to wake

Commands:

adb shell am force-stop com.example.app
adb shell am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://app.example.com/task/42"
adb shell am start -W -a android.intent.action.VIEW -d "myapp://open/monitor"
adb shell pm get-app-links com.example.app
adb shell dumpsys activity activities | grep -i com.example.app
adb shell dumpsys activity services com.example.app
adb shell dumpsys notification | grep -i com.example.app
adb shell dumpsys jobscheduler | grep -i com.example.app
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -p com.example.app
adb logcat -v brief | grep -iE "ForegroundService|SecurityException|BackgroundServiceStart"
adb shell dumpsys deviceidle force-idle
adb shell pm grant com.example.app android.permission.POST_NOTIFICATIONS

After launch, track: per-source launch success rate and parameter arrival rate, cold start to first frame time, per-device and ROM average FGS survival time, recovery source distribution, recovery success rate and duplicate execution rate (server-side dedupe by dedupeKey), permission and whitelist consent rate, user-initiated stop rate, plus battery and abnormal wake count. These derive from launch(route, source), fgs_start, recovery(trigger, success, elapsedMs), consent, stop events. Key is don't turn metrics into personal info collection.

Things Not Done Yet

Top three priorities: update regression matrix for Android 16/17 behavior changes, esp. job quota, while-in-use services, and --debug-link diagnostics; each targetSdk upgrade run version table. Migrate "user-initiated large data transfers" from FGS to user-initiated data transfer job to bypass quota and duration limits. Unify vendor pushes behind a PushPort interface for consistent "arrive → resumeIfNeeded" semantics, highest wake success rate, cleanest code.

Further: state layer from SharedPreferences to Room, atomic checkpoint and remove(dedupeKey) via transactions; connect client dedupeKey with server-side idempotency table, do full-chain stress test of "kill → recover → duplicate report"; if routing layer migrates to Compose, have Navigation consume Route, eliminating multi-activity and taskAffinity headaches.

Engineering governance: CI static checks for missing exported, Manifest/typeMask mismatch, keep-alive keywords, move compliance to compile time; A/B test whitelist guidance copy by ROM; adaptive heartbeat for long connections based on network and Doze, exponential backoff and jitter; dashboard for metrics, drill down by ROM, OS version, app version.

Finally, a self-test: any domestic ROM phone, four steps: cold start from SMS link, detail correct, back to home; enable sync, notification visible with "Stop"; lock screen and switch away, adb shell am force-stop to simulate kill; wait for watchdog/push or reopen, task resumes from breakpoint, no duplicate. First three steps via API, fourth via design—if fourth passes, this system stands.

References

System behavior ultimately depends on your own targetSdk and target devices; package names, domains, fingerprints are placeholders, remember to replace.