Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
225 commits
Select commit Hold shift + click to select a range
93631d6
feat(accounts): add platform credential slots
veryCrunchy Sep 1, 2026
cf90d81
chore(changelog): link pull request
veryCrunchy Sep 1, 2026
32f6c79
fix(accounts): serialize credential lifecycle state
veryCrunchy Sep 1, 2026
eee6c83
fix(accounts): preserve retained account recovery
veryCrunchy Sep 3, 2026
d62cea8
fix(android): defer retained-account uploads
veryCrunchy Sep 3, 2026
5f17d3f
fix(accounts): harden credential recovery races
veryCrunchy Sep 3, 2026
83ea0d9
fix(accounts): coordinate background account work
veryCrunchy Sep 3, 2026
04c2a0a
fix(accounts): close credential lifecycle races
veryCrunchy Sep 3, 2026
4f6fa2a
fix(accounts): close remaining removal races
veryCrunchy Sep 4, 2026
1c31b71
fix(accounts): preserve credential recovery invariants
veryCrunchy Sep 4, 2026
cec7136
fix(accounts): retire account-bound transfer resources
veryCrunchy Sep 4, 2026
1e404f6
fix(accounts): preserve active resource identity
veryCrunchy Sep 4, 2026
6a1f33e
fix(android): serialize account-bound writebacks
veryCrunchy Sep 4, 2026
5dcc9c0
fix(accounts): retain credential recovery paths
veryCrunchy Sep 4, 2026
30941b0
fix(android): close credential recovery gaps
veryCrunchy Sep 4, 2026
3965ead
fix(accounts): close remaining transition races
veryCrunchy Sep 4, 2026
b477088
fix(accounts): retire removed account resources
veryCrunchy Sep 4, 2026
32fd78b
fix(accounts): close recovery transition gaps
veryCrunchy Sep 4, 2026
1cbd3fc
fix(accounts): harden credential recovery boundaries
veryCrunchy Sep 4, 2026
5040557
fix(accounts): serialize account-bound mutations
veryCrunchy Sep 4, 2026
de408fa
fix(accounts): preserve removal recovery state
veryCrunchy Sep 4, 2026
44e8071
fix(accounts): preflight account-bound transitions
veryCrunchy Sep 4, 2026
4a851d9
fix(accounts): close retained-session safety gaps
veryCrunchy Sep 4, 2026
0e96293
fix(accounts): finish committed account transitions
veryCrunchy Sep 4, 2026
aa76381
fix(accounts): isolate credential recovery mutations
veryCrunchy Sep 4, 2026
6b9ff27
fix(accounts): preflight sync ownership on removal
veryCrunchy Sep 4, 2026
262127b
fix(accounts): reject unsupported desktop registries
veryCrunchy Sep 4, 2026
628d2ca
fix(accounts): retain removal cleanup ownership
veryCrunchy Sep 4, 2026
db72523
fix(accounts): finish removal recovery
veryCrunchy Sep 4, 2026
d4db556
fix(accounts): harden transition recovery
veryCrunchy Sep 4, 2026
9ecd00c
fix(accounts): harden damaged state recovery
veryCrunchy Sep 4, 2026
b54d88d
fix(accounts): complete removal isolation
veryCrunchy Sep 5, 2026
f721f91
test(accounts): verify durable root cleanup
veryCrunchy Sep 5, 2026
f198eb0
fix(accounts): harden removal recovery
veryCrunchy Sep 5, 2026
8b11831
fix(accounts): fail closed on recovery tombstones
veryCrunchy Sep 5, 2026
10157c7
test(accounts): split removal recovery coverage
veryCrunchy Sep 5, 2026
fbc8764
fix(accounts): quiesce Linux writes before removal
veryCrunchy Sep 5, 2026
5e6da1b
fix(accounts): persist desktop cleanup markers first
veryCrunchy Sep 5, 2026
0eb5e5d
fix(accounts): recover Android accounts before enumeration
veryCrunchy Sep 5, 2026
97e9e13
fix(accounts): retry Android removal cleanup
veryCrunchy Sep 5, 2026
a847037
test(desktop): inject FUSE ownership in lifecycle tests
veryCrunchy Sep 5, 2026
83e6b7b
fix(accounts): harden Android removal recovery
veryCrunchy Sep 5, 2026
d6dfd46
fix(uploads): preserve retained account queues
veryCrunchy Sep 5, 2026
8c370d3
fix(accounts): journal desktop credential transitions
veryCrunchy Sep 5, 2026
c405223
refactor(desktop): decouple Linux cleanup lifecycle
veryCrunchy Sep 5, 2026
908507d
fix(desktop): clear recovered Cloud Files failures
veryCrunchy Sep 5, 2026
5b536d7
fix(accounts): preserve distinct legacy cleanup keys
veryCrunchy Sep 5, 2026
48f99de
test(desktop): invoke lifecycle failures explicitly
veryCrunchy Sep 5, 2026
84298c4
fix(desktop): count lazy hydration as live
veryCrunchy Sep 5, 2026
260d2bd
fix(accounts): quarantine malformed removal journals
veryCrunchy Sep 5, 2026
1daa249
fix(accounts): close Android removal races
veryCrunchy Sep 5, 2026
2d6fcb6
fix(desktop): finish sign-out after revocation
veryCrunchy Sep 5, 2026
9d9e98f
fix(sync): retire account SAF state safely
veryCrunchy Sep 5, 2026
dc2635c
fix(sharing): defer uploads for unreadable accounts
veryCrunchy Sep 5, 2026
99ab7be
fix(desktop): quiesce Cloud Files on sign-out
veryCrunchy Sep 5, 2026
1f03da9
refactor(accounts): share retained identity policy
veryCrunchy Sep 5, 2026
eb003fb
fix(desktop): return quiescence outcome
veryCrunchy Sep 5, 2026
d8bb7af
test(desktop): prove callback drain ordering
veryCrunchy Sep 5, 2026
3008c67
fix(desktop): disable reads after failed FUSE unmount
veryCrunchy Sep 5, 2026
9963216
fix(android): recover credential slots before selection
veryCrunchy Sep 5, 2026
ee428b0
fix(android): complete account retirement cleanup
veryCrunchy Sep 5, 2026
7f37455
fix(desktop): harden account removal recovery
veryCrunchy Sep 5, 2026
e75f752
fix(accounts): preserve unreadable retained slots
veryCrunchy Sep 5, 2026
2253d2e
fix(android): quiesce account file cache reads
veryCrunchy Sep 5, 2026
4f13f42
fix(accounts): preserve desktop rollback selection
veryCrunchy Sep 5, 2026
aa4e460
fix(accounts): retry desktop removal cleanup
veryCrunchy Sep 5, 2026
036511d
fix(desktop): remove accounts without loading secrets
veryCrunchy Sep 5, 2026
81e33d9
fix(windows): purge removed Cloud Files roots
veryCrunchy Sep 5, 2026
bc5a26e
fix(android): revoke handoffs across account transitions
veryCrunchy Sep 5, 2026
b2b1f83
fix(accounts): retain preview cleanup recovery
veryCrunchy Sep 5, 2026
9065961
test(accounts): fix preview cleanup fixtures
veryCrunchy Sep 5, 2026
1b0a940
fix(accounts): clear unreadable active ownership
veryCrunchy Sep 5, 2026
56d4800
fix(accounts): preserve unreadable removal rollback
veryCrunchy Sep 5, 2026
396609f
fix(accounts): retain original preview retry identity
veryCrunchy Sep 5, 2026
d447aa4
fix(uploads): retain jobs for unreadable account registry
veryCrunchy Sep 5, 2026
5221e8d
fix(desktop): recover malformed cleanup phases
veryCrunchy Sep 5, 2026
a37b2c4
fix(android): finish committed account selection
veryCrunchy Sep 5, 2026
ea9acbd
fix(accounts): purge retained mutation recovery
veryCrunchy Sep 5, 2026
330ed9f
fix(accounts): persist mutation cleanup retries
veryCrunchy Sep 5, 2026
ba32b37
fix(account): purge private state after removal
veryCrunchy Sep 5, 2026
139c220
chore(website): refresh marketing captures
obiente-automations[bot] Sep 5, 2026
8858e08
fix(account): serialize private-state publication
veryCrunchy Sep 5, 2026
e04fd66
fix(account): purge removal residual state
veryCrunchy Sep 5, 2026
a2d4b09
fix(desktop): clear legacy handoffs on removal
veryCrunchy Sep 5, 2026
6c240a4
test(android): make media cleanup deterministic
veryCrunchy Sep 5, 2026
71c60c8
fix(account): close residual cleanup races
veryCrunchy Sep 5, 2026
8c51d43
fix(desktop): reserve external handoff cache budget
veryCrunchy Sep 5, 2026
3ea964b
chore(website): refresh marketing captures
obiente-automations[bot] Sep 5, 2026
ecaf99e
fix(desktop): recover credential removals
veryCrunchy Sep 5, 2026
cb6c6e6
fix(desktop): validate handoff paths portably
veryCrunchy Sep 5, 2026
bcd1836
fix(accounts): retire deck draft state
veryCrunchy Sep 5, 2026
bf4f21f
fix(desktop): delete read-only handoff copies
veryCrunchy Sep 5, 2026
5598786
fix(accounts): harden retained account recovery
veryCrunchy Sep 5, 2026
8e55759
fix(accounts): gate credential recovery transitions
veryCrunchy Sep 6, 2026
c167e28
chore(architecture): lower Android service baseline
veryCrunchy Sep 6, 2026
f501e08
fix(android): serialize conditional file creation
veryCrunchy Sep 6, 2026
bc317fa
test(android): cover offline account recovery policy
veryCrunchy Sep 6, 2026
449489f
refactor(desktop): preserve compact session boundary
veryCrunchy Sep 6, 2026
f3a26a1
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
6383844
fix(accounts): purge removed private state
veryCrunchy Sep 6, 2026
ca9b1d9
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
c4b9068
fix(accounts): purge private state on removal
veryCrunchy Sep 6, 2026
c54a0ad
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
7055dd5
fix(accounts): fence private cache retirement
veryCrunchy Sep 6, 2026
f2c5359
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
2f09ad2
fix(accounts): finish private state cleanup
veryCrunchy Sep 6, 2026
32f97e9
fix(desktop): clear removed account state
veryCrunchy Sep 6, 2026
fd10112
fix(ui): validate workspace cleanup identity
veryCrunchy Sep 6, 2026
a626ade
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
f3c0647
fix(accounts): close remaining retirement races
veryCrunchy Sep 6, 2026
412f36b
fix(accounts): close desktop cleanup gaps
veryCrunchy Sep 6, 2026
5d63343
fix(accounts): quiesce android private activity
veryCrunchy Sep 6, 2026
31c7c7e
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
eadef3d
fix(android): reuse account lease for provider mutation lookups
veryCrunchy Sep 9, 2026
3bdd646
fix(android): preserve account-owned background uploads
veryCrunchy Sep 1, 2026
a770194
chore(changelog): link account background upload fix
veryCrunchy Sep 1, 2026
1843de9
docs(platform): bind uploads to supplied sessions
veryCrunchy Sep 4, 2026
2f7800b
refactor(platform): keep upload contract compact
veryCrunchy Sep 4, 2026
3de794e
fix(android): honor durable upload account lease
veryCrunchy Sep 4, 2026
f63bda7
refactor(android): own durable upload execution
veryCrunchy Sep 4, 2026
d400a48
fix(uploads): recover worker account metadata
veryCrunchy Sep 4, 2026
5cfe4d8
fix(uploads): defer ambiguous account recovery
veryCrunchy Sep 4, 2026
47a915e
fix(uploads): retire removed account work
veryCrunchy Sep 5, 2026
41ade6c
refactor(accounts): keep registry adapter bounded
veryCrunchy Sep 5, 2026
7b9e6cc
chore(website): refresh marketing captures
obiente-automations[bot] Sep 5, 2026
d5c6611
refactor(accounts): keep Android services bounded
veryCrunchy Sep 5, 2026
c7a93dc
refactor(android): keep upload resolution bounded
veryCrunchy Sep 5, 2026
b04f8b6
fix(uploads): contain corrupt registry preference
veryCrunchy Sep 5, 2026
93752cf
fix(uploads): retry corrupt registry preference
veryCrunchy Sep 5, 2026
54386f6
refactor(platform): keep recovery contract bounded
veryCrunchy Sep 5, 2026
7367e96
fix(accounts): preserve background recovery state
veryCrunchy Sep 6, 2026
d815af9
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
58ee098
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
147e3c4
fix(platform): honor the source size ceiling
veryCrunchy Sep 6, 2026
bfcfd0f
fix(uploads): quarantine unreadable queues
veryCrunchy Sep 6, 2026
3f6a01a
fix(uploads): stop retrying malformed registries
veryCrunchy Sep 6, 2026
c37a6f5
fix(android): preserve queued upload scheduling
veryCrunchy Sep 1, 2026
017ac03
chore(changelog): link upload scheduling fix
veryCrunchy Sep 1, 2026
4706996
fix(android): restore queued uploads at startup
veryCrunchy Sep 1, 2026
f6c447d
fix(android): retain startup upload retries
veryCrunchy Sep 3, 2026
508eb69
fix(android): retain upload reconciliation after journal errors
veryCrunchy Sep 4, 2026
1ab9847
fix(android): defer uploads during credential recovery
veryCrunchy Sep 4, 2026
291f796
fix(uploads): skip WorkManager-owned recovery jobs
veryCrunchy Sep 4, 2026
f6f1ca9
fix(uploads): bound credential recovery retries
veryCrunchy Sep 4, 2026
01a43e8
fix(uploads): recover registry before worker rejection
veryCrunchy Sep 4, 2026
a75a06b
fix(uploads): keep credential recovery deferred
veryCrunchy Sep 4, 2026
8969213
fix(uploads): bound startup recovery diagnostics
veryCrunchy Sep 4, 2026
e2c0806
fix(uploads): wake failed scheduling recovery
veryCrunchy Sep 4, 2026
984982c
fix(uploads): wake recovery after worker failure
veryCrunchy Sep 5, 2026
6feb0d9
fix(uploads): close recovery wakeup races
veryCrunchy Sep 5, 2026
3b0c3f8
fix(uploads): centralize queued status recovery
veryCrunchy Sep 5, 2026
428514b
fix(uploads): back off worker recovery
veryCrunchy Sep 5, 2026
19aa454
fix(uploads): defer transient source failures
veryCrunchy Sep 5, 2026
4b51467
fix(uploads): fail permanently unavailable sources
veryCrunchy Sep 5, 2026
adfea37
fix(uploads): release cancelled unowned selections
veryCrunchy Sep 5, 2026
b413b94
fix(uploads): retry terminal capability cleanup
veryCrunchy Sep 5, 2026
4a1c61c
fix(uploads): retain pending capability cleanup
veryCrunchy Sep 5, 2026
2cdfd55
test(uploads): cover legacy cleanup marker
veryCrunchy Sep 5, 2026
0e2cb3e
fix(uploads): recover pending capability cleanup
veryCrunchy Sep 5, 2026
d7f9b4b
test(uploads): keep cleanup cancellation test void
veryCrunchy Sep 5, 2026
7c54894
fix(uploads): decouple terminal cleanup recovery
veryCrunchy Sep 5, 2026
c08afb5
fix(uploads): validate persisted cleanup marker
veryCrunchy Sep 5, 2026
5de864e
fix(uploads): run terminal cleanup offline
veryCrunchy Sep 5, 2026
f03c0fb
fix(uploads): preserve cleanup with corrupt registry
veryCrunchy Sep 5, 2026
06d3590
fix(uploads): retain unreadable capability metadata
veryCrunchy Sep 5, 2026
e2cd3e0
fix(uploads): clean cancelled picker grants
veryCrunchy Sep 5, 2026
baa3271
fix(uploads): release undelivered picker selections
veryCrunchy Sep 5, 2026
4a4eba7
fix(uploads): recover orphaned picker grants
veryCrunchy Sep 5, 2026
989b704
fix(uploads): preserve immediate recovery intent
veryCrunchy Sep 5, 2026
ff8222a
fix(uploads): enforce picker capability limit
veryCrunchy Sep 5, 2026
eba8536
fix(uploads): consume scheduling wakeups atomically
veryCrunchy Sep 5, 2026
29b63fd
fix(uploads): defer capability metadata read failures
veryCrunchy Sep 5, 2026
96e90b0
fix(uploads): reject malformed capability metadata
veryCrunchy Sep 5, 2026
9b60b71
fix(uploads): isolate malformed picker capabilities
veryCrunchy Sep 6, 2026
b20aceb
fix(uploads): protect owned malformed capabilities
veryCrunchy Sep 6, 2026
3af2a62
test(uploads): split account resolution coverage
veryCrunchy Sep 6, 2026
df6d4b2
fix(uploads): reject corrupt capability ciphertext
veryCrunchy Sep 6, 2026
d276894
refactor(platform): preserve service size boundary
veryCrunchy Sep 6, 2026
323ed12
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
97e529d
fix(uploads): bound capability recovery
veryCrunchy Sep 6, 2026
815a974
fix(uploads): coalesce terminal recovery
veryCrunchy Sep 6, 2026
94b3f5c
fix(uploads): quarantine unknowable capability cleanup
veryCrunchy Sep 6, 2026
f4216a3
fix(uploads): terminate quarantined cleanup
veryCrunchy Sep 6, 2026
d553758
fix(uploads): bound storage and preserve recovery backoff
veryCrunchy Sep 9, 2026
50db150
fix(uploads): finish stable malformed-peer cleanup
veryCrunchy Sep 9, 2026
525d7d2
fix(files): recover folder grant ownership
veryCrunchy Sep 5, 2026
29b1ea8
fix(accounts): retire folder grant ownership
veryCrunchy Sep 5, 2026
d475081
fix(files): reconcile ambiguous grant removal
veryCrunchy Sep 5, 2026
0e9a285
fix(files): retry folder grant cleanup
veryCrunchy Sep 5, 2026
3051fb8
fix(files): reconcile orphaned upload cleanup
veryCrunchy Sep 5, 2026
5f9a081
chore(changelog): link folder grant recovery
veryCrunchy Sep 5, 2026
2be0ca7
fix(files): recover ambiguous capability binding
veryCrunchy Sep 6, 2026
9fae563
chore(website): refresh marketing captures
obiente-automations[bot] Sep 5, 2026
c8ff76d
test(android): update account retirement cleanup contract
veryCrunchy Sep 6, 2026
3c85cae
fix(android): preserve folder sync setup recovery
veryCrunchy Sep 6, 2026
f0f7ddf
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
2a44471
refactor(android): preserve sync engine boundary
veryCrunchy Sep 6, 2026
8a221b1
chore(website): refresh marketing captures
obiente-automations[bot] Sep 6, 2026
3ddf12d
fix(android): preserve restored folder capabilities
veryCrunchy Sep 6, 2026
0cf9b0d
fix(android): confirm committed folder pair setup
veryCrunchy Sep 6, 2026
fc550cf
fix(accounts): fence complete dynamic read lifetimes
veryCrunchy Sep 9, 2026
ef0f432
fix(accounts): persist credential rollback completion
veryCrunchy Sep 9, 2026
6fe80d0
fix(deck): retire legacy drafts before replacement
veryCrunchy Sep 9, 2026
a64f83b
fix(deck): retain legacy submission tombstones during cleanup
veryCrunchy Sep 9, 2026
8d1c884
Merge branch 'feature/account-credential-slots' into fix/account-back…
veryCrunchy Sep 9, 2026
2530237
fix(android): bound upload scheduling recovery passes
veryCrunchy Sep 9, 2026
c568dca
Merge branch 'fix/account-background-isolation' into fix/durable-uplo…
veryCrunchy Sep 9, 2026
1662b7a
test(android): group scheduling monitor regressions by owner
veryCrunchy Sep 9, 2026
9d2c9f0
fix(android): coalesce cleanup recovery self wakeups
veryCrunchy Sep 9, 2026
b9e41c5
Merge branch 'fix/durable-upload-scheduling-recovery-stack' into fix/…
veryCrunchy Sep 9, 2026
6302932
chore(website): refresh marketing captures
obiente-automations[bot] Sep 9, 2026
9cef5bb
fix(files): preserve and restore folder capability ownership
veryCrunchy Sep 13, 2026
0d7df78
fix(sync): integrate current account and upload recovery
veryCrunchy Sep 14, 2026
daf2925
fix(desktop): retain account-aware folder chooser contract
veryCrunchy Sep 14, 2026
f1ee048
chore(website): refresh marketing captures
obiente-automations[bot] Sep 14, 2026
033f58a
fix(android): durably retry abandoned folder access cleanup
veryCrunchy Sep 14, 2026
ea274a0
chore(website): refresh marketing captures
obiente-automations[bot] Sep 14, 2026
589880f
fix(android): integrate account recovery with folder capabilities
veryCrunchy Sep 14, 2026
c004fc7
chore(website): refresh marketing captures
obiente-automations[bot] Sep 14, 2026
4eaaaea
fix(android): preserve folder selection recovery through lifecycle ch…
veryCrunchy Sep 14, 2026
8d0d25a
chore(website): refresh marketing captures
obiente-automations[bot] Sep 14, 2026
ab4d121
fix(android): complete independent folder capability recovery
veryCrunchy Sep 14, 2026
077edfb
docs: keep folder recovery summary within release limits
veryCrunchy Sep 14, 2026
4e000e7
fix(android): preserve account cleanup failures and cancellation
veryCrunchy Sep 14, 2026
b528b70
fix(android): complete recovery without suppressing cancellation
veryCrunchy Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions ADAPTER_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,33 @@ Each boundary has a corresponding test responsibility:
permission denial, confirmation, adaptive layout, and keyboard/touch access.
- Platform tests cover credential stores, filesystem paths and providers,
background scheduling, external handoff, packaging, and lifecycle recovery.

Android folder capability cleanup uses demand-driven one-time WorkManager work.
Empty stores and committed pairs do not keep cleanup work alive. Outstanding
selections retain a retry owner until bound or abandoned; reconciliation preserves
selections already delivered to an open setup.
Folder-picker acquisition and durable scheduling run on the picker's owned IO
scope, with result delivery on Main and cancellation cleanup retained on IO.
New acquisitions and cleanup requests schedule recovery, and unfinished cleanup
retains bounded WorkManager backoff.
A process restoration grace period protects pending folder drafts only while
acquiring or ready selections remain after reconciliation; completed setup and
cleanup return without waiting. A cancelled pair save preserves authoritative
ownership recovery and then rethrows cancellation, even when the save committed.
Abandoned acquisitions and committed pair removals retain cleanup evidence until access
is released. Cleanup retries do not transfer or delete user file contents.
- Live-server audits use synthetic disposable accounts, record exact tested
versions, and remain separate from deterministic unit and integration tests.
A durably removed Android folder-sync pair reports completion while its previously
scheduled capability recovery worker retries any remaining permission cleanup.
Ambiguous coordinator saves still require authoritative confirmation of removal.
Account retirement remains strict until its capability cleanup finishes.
Cancellation from grant, storage, and cipher adapters remains cancellation rather
than being reported as damaged recovery metadata or deferred cleanup.
Reconciliation records independent cleanup progress before reporting a failed
provider, so one unavailable grant cannot indefinitely retain unrelated grants.
A legacy shared root can regain expired access only for an account that still
owns a recorded pair at that exact root in the authoritative coordinator.

A bug fix adds the smallest regression test at the layer where the invariant
failed. Tests should assert public behavior, not copied implementation details.
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dev.obiente.nextcloudnative

import android.content.Context
import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

internal fun startAndroidFileSyncCapabilityRecovery(
context: Context,
scope: CoroutineScope,
load: () -> AndroidFileSyncPersistedState,
capabilities: AndroidFileSyncCapabilityLifecycle,
) {
scope.launch {
try {
// Retire the old unconditional periodic schedule after upgrading.
WorkManager.getInstance(context).cancelUniqueWork("file-sync-capability-cleanup-v1").await()
AndroidFileSyncEngine.ENGINE_LOCK.withLock {
capabilities.reconcile(load())
if (capabilities.hasRecoveryWork()) requestAndroidFileSyncCapabilityRecovery(context)
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (failure: Exception) {
// A failed immediate cleanup must still have a durable retry owner.
try {
requestAndroidFileSyncCapabilityRecovery(context)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (schedulingFailure: Exception) {
failure.addSuppressed(schedulingFailure)
}
android.util.Log.w("FolderCapabilityRecovery", "Folder access cleanup is awaiting recovery.", failure)
}
}
}

internal fun requestAndroidFileSyncCapabilityRecovery(context: Context) {
val request = OneTimeWorkRequestBuilder<AndroidFileSyncCapabilityRecoveryWorker>()
.setInitialDelay(1, TimeUnit.MINUTES)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
// Append preserves a new request arriving while the previous worker finishes.
val operation = WorkManager.getInstance(context).enqueueUniqueWork(
"file-sync-capability-cleanup-v2", ExistingWorkPolicy.APPEND_OR_REPLACE, request,
)
// This boundary runs on the owned IO path, never the Activity result callback.
runBlocking { withTimeout(30_000L) { operation.await() } }
}

internal class AndroidFileSyncCapabilityRecoveryWorker(context: Context, parameters: WorkerParameters) :
CoroutineWorker(context, parameters) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
try {
val capabilities = AndroidFileSyncCapabilityLifecycle(applicationContext)
val store = AndroidFileSyncStore(applicationContext)
// A worker may start the process before the activity restores its draft.
reconcileFileSyncCapabilitiesAfterRestoration(
AndroidFileSyncEngine.ENGINE_LOCK, store::loadAndReconcileUploadCleanups, capabilities,
onFailure = { throw it },
)
Comment thread
veryCrunchy marked this conversation as resolved.
AndroidFileSyncEngine.ENGINE_LOCK.withLock {
if (capabilities.hasRecoveryWork()) Result.retry() else Result.success()
}
} catch (cancelled: CancellationException) {
throw cancelled
} catch (_: Exception) {
Result.retry()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ internal class AndroidFileSyncEngine(context: Context) {
private val scheduledMediaReconciliations = ConcurrentHashMap.newKeySet<String>()
private val scheduledPairScheduling = DeferredFileSyncPairSchedulingRegistry()
private val stagingRoot = File(appContext.cacheDir, "file-sync-staging")
private val capabilities = AndroidFileSyncCapabilityLifecycle(appContext)
private val loadCapabilityState = store::loadAndReconcileUploadCleanups
init { startAndroidFileSyncCapabilityRecovery(appContext, reconciliationScope, loadCapabilityState, capabilities) }

suspend fun loadCenter(
session: NextcloudSession,
Expand Down Expand Up @@ -257,14 +260,9 @@ internal class AndroidFileSyncEngine(context: Context) {
val normalizedRemote = normalizeRemoteRoot(remoteRootPath)
val accountId = NextcloudDocumentIds.accountKey(session)
val current = store.load()
if (current.coordinator.pairs.any {
it.accountId == accountId &&
it.localRootId == localRoot.localRootId &&
it.remoteRootPath == normalizedRemote
}
) {
if (hasDuplicateAndroidFileSyncRoot(current.coordinator.pairs, accountId, localRoot.localRootId, normalizedRemote)) {
return@withLock FileSyncCenterActionResult.Rejected(
"That local and Nextcloud folder pair already exists.",
"That local folder already belongs to a folder sync pair.",
)
}
val pair = FileSyncPair(
Expand All @@ -274,14 +272,22 @@ internal class AndroidFileSyncEngine(context: Context) {
remoteRootPath = normalizedRemote,
configuration = configuration,
)
store.save(
current.copy(
coordinator = addFileSyncPair(current.coordinator, pair),
localDisplayNames = current.localDisplayNames + (pair.id to localRoot.displayName),
),
val updated = current.copy(
coordinator = addFileSyncPair(current.coordinator, pair),
localDisplayNames = current.localDisplayNames + (pair.id to localRoot.displayName),
)
scheduler.schedule(pair.id, accountId, userId, pair.configuration)
FileSyncCenterActionResult.Completed("Folder sync pair added. Run it to review the first sync.")
if (localRoot.localRootId.startsWith("content://")) {
bindAndPersistFileSyncPair(
pairId = pair.id,
bindReady = { capabilities.bindReady(AndroidFileSyncCapabilityAccountId(accountId), localRoot.localRootId, pair.id) },
persist = { store.save(updated) },
load = store::load,
abandonUncommittedPair = capabilities::abandonUncommittedPair,
)
} else {
store.save(updated)
}
committedFileSyncPairResult { scheduler.schedule(pair.id, accountId, userId, pair.configuration) }
}

private fun FileSyncConfiguration.scheduleDescription(): String {
Expand Down Expand Up @@ -312,8 +318,7 @@ internal class AndroidFileSyncEngine(context: Context) {
"This folder sync pair belongs to another account.",
)
}
val releasesLocalGrant = pair.localRootId.startsWith("content://") &&
current.coordinator.pairs.none { it.id != pairId && it.localRootId == pair.localRootId }
capabilities.reconcile(current)
var cleanedCoordinator: FileSyncCoordinatorState? = null
var remoteCleanupRejected = false
val removed = removeConfiguredFileSyncPair(
Expand Down Expand Up @@ -351,18 +356,14 @@ internal class AndroidFileSyncEngine(context: Context) {
}
},
persistRemoval = {
capabilities.preparePairCleanup(pairId)
val remaining = removeFileSyncPair(requireNotNull(cleanedCoordinator), pairId)
store.save(
current.copy(
coordinator = remaining,
localDisplayNames = current.localDisplayNames - pairId,
),
)
capabilities.persistPairRemoval(pairId, store::loadAndReconcileUploadCleanups) {
store.save(current.copy(coordinator = remaining, localDisplayNames = current.localDisplayNames - pairId))
}
},
cancelSchedule = { scheduler.cancel(pairId) },
releaseLocalGrant = {
releaseSafGrantAfterPairRemoval(appContext, pair.localRootId, releasesLocalGrant)
},
releaseLocalGrant = { capabilities.finishPairCleanupOrRetry(pairId, allowDeferredCleanup = true, load = store::load) },
)
if (!removed) {
return@withLock FileSyncCenterActionResult.Rejected(if (remoteCleanupRejected) {
Expand Down
Loading