fix(upgrade): Add a persistent acknowledgement safety net for Play purchases

Play auto-refunds (and revokes) purchases not acknowledged within 3 days.
The in-process ack machinery covers every case where the process lives
long enough; what it cannot cover is a process death around the Play
sheet (aggressive OEM task killers) followed by the user not reopening
the app before the deadline.

Add a gplay-only WorkManager safety net:
- PurchaseAckWorker: self-completing sweep via a new bounded
  BillingManager.ensureAllAcknowledged() that refreshes and acknowledges
  in the same coroutine (the reactive ack collector is async, so a worker
  cannot prove its acks happened through it). Retries with exponential
  backoff until the purchase's refund deadline, then gives up visibly.
- PurchaseAckScheduler: two unique work identities. A launch watch
  (REPLACE, armed and awaited before launchBillingFlow with a 30min delay
  so it cannot complete while the user is still in the sheet) and a
  discovered-purchase rescue (KEEP, 1min delay, armed directly from an
  ack pass that finds unacknowledged purchases, pre-attempt). Separate
  identities so a new purchase flow can never displace a pending rescue.
  Both triggers are fail-open: a broken WorkManager never blocks a
  purchase or an ack. WorkManager resolves via Provider at first arm so
  eager Application-time construction of the billing stack cannot
  trigger WorkManager's on-demand initialization prematurely.
- Nothing cancels the work from the foreground path: an ack pass can see
  zero unacked purchases while the sheet is still open, so the worker
  completes itself after its own reconciliation instead.

The ack pass now runs under a mutex (the worker sweep and the reactive
collector would otherwise race the token bookkeeping) and reports
per-outcome counts for the sweep result mapping.

This is a port of d4rken-org/sdmaid-se#2685; the ported sources are
byte-identical to the donor apart from the package rename.

CAPod had no explicit WorkManager wiring at all (work-runtime only
arrived transitively through Glance), so this also adds it:
- addWorkerManager() pinning androidx.work 2.7.1, the version already
  resolved via Glance, plus androidx.hilt:hilt-work and its KSP
  compiler. work-runtime-ktx is required at 2.7.1: CoroutineWorker,
  Operation.await, OneTimeWorkRequestBuilder and workDataOf all still
  live in the ktx artifact at that version. androidx.hilt moves 1.0.0 ->
  1.2.0 (by conflict resolution) because 1.0.0's hilt-compiler ships no
  KSP SymbolProcessorProvider, so @HiltWorker would generate nothing.
- WorkManagerModule providing the singleton WorkManager.
- App implements Configuration.Provider with the injected
  HiltWorkerFactory. WorkManager 2.7.1 still declares that interface as
  getWorkManagerConfiguration(), not the later property form.
- The manifest removes androidx.work's startup initializer so the
  on-demand configuration is the one that takes effect.

FOSS stays untouched behaviour-wise: all new billing types live in
src/gplay, workers need no manifest entry, and the worker factory
resolves the worker only in gplay variants.
This commit is contained in:
darken
2026-08-18 17:55:19 +02:00
committed by Matthias Urhahn
parent cebed0a60d
commit 971fcbd34c
13 changed files with 613 additions and 9 deletions
+16
View File
@@ -130,6 +130,22 @@ fun DependencyHandlerScope.addDataStore() {
implementation("androidx.datastore:datastore-preferences:1.1.4")
}
fun DependencyHandlerScope.addWorkerManager() {
// Resolved transitively via Glance today; declared explicitly so the safety-net worker does not
// depend on Glance's choice. work-runtime-ktx is NOT an empty shell at this version: at 2.7.1
// CoroutineWorker, OperationKt.await, OneTimeWorkRequestBuilder and workDataOf all live in the
// ktx artifact (they only moved into work-runtime on later releases).
val version = "2.7.1"
implementation("androidx.work:work-runtime:$version")
implementation("androidx.work:work-runtime-ktx:$version")
testImplementation("androidx.work:work-testing:$version")
// androidx.hilt 1.0.0's hilt-compiler ships no KSP SymbolProcessorProvider, so @HiltWorker
// would silently generate nothing under this project's KSP setup.
implementation("androidx.hilt:hilt-work:1.2.0")
ksp("androidx.hilt:hilt-compiler:1.2.0")
}
fun DependencyHandlerScope.addGlance() {
implementation("androidx.glance:glance-appwidget:${Versions.Glance.core}")
implementation("androidx.glance:glance-material3:${Versions.Glance.core}")