Skip to content

simplify AccountVerificationActivity& CapabiliesWorker + fix bugs - #6736

Open
mahibi wants to merge 8 commits into
masterfrom
avidNullCapabilities
Open

mahibi wants to merge 8 commits into
masterfrom
avidNullCapabilities

Conversation

@mahibi

@mahibi mahibi commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Investigates and fixes the various ways Capabilities could end up null/stale for a user, and cleans up the account-verification flow
along the way.

Capabilities fetching (CapabilitiesWorker → split)

  • CapabilitiesWorker always returned Result.success() even when the fetch failed, so a failed single-account fetch (e.g. right
    after login) silently waited up to 12h for the next periodic run before retrying. It also silently swallowed a response with no
    ocs/data/capabilities payload — no retry, no error.
  • Migrated it to Kotlin/coroutines and split it by actual retry contract, since one class was conflating two different jobs:
  • CapabilitiesSyncWorker: best-effort refresh across every account (app-start chain, 12h periodic sync, manual Settings refresh) —
    always succeeds, never blocks dependent work.
  • CapabilitiesFetcher: the shared fetch/persist logic, kept as a plain class so it's testable with mocks, no
    WorkManager/Robolectric needed.
  • Added CapabilitiesFetcherTest covering the success path and every failure branch, including the previously-silent one.

Account verification (AccountVerificationActivity)

  • Root-caused why a freshly created, already-valid account could get deleted: a redundant capabilities re-fetch ran right after signup,
    and a transient failure of that throwaway re-check called abortVerification(), which deletes the account — even though its
    capabilities were already correctly stored one step earlier. Fixed by treating that failure as non-fatal, like the sibling
    push-registration/signaling-settings steps already do.
  • Traced this further via git history: storeProfile() has embedded the already-fetched capabilities directly since Oct 2022
    (b6e9c9d56, a fix for a different null-capabilities crash), which made the entire separate capabilities re-fetch step redundant — it
    was left in place instead of being removed. Removed it outright: storeProfile() now proceeds straight to push-notification setup, and
    CapabilitiesFetchWorker (its only caller) is deleted.
  • Replaced the EventBus-based PROFILE_STORED/PUSH_REGISTRATION self-signaling with direct calls — neither ever crossed into a
    Worker, so routing them through a global bus only added indirection and forced manual filtering of unrelated accounts' events.
    CAPABILITIES_FETCH(now removed)/SIGNALING_SETTINGS stayed on EventBus since those are genuinely posted from a background
    Worker.
  • Migrated all of AccountVerificationActivity's RxJava (Observer/MaybeObserver callback chains) to coroutines/lifecycleScope,
    removing the manual Disposable bookkeeping and runOnUiThread wrapping in the process.
  • General readability pass: extracted repeated patterns (getUser()/getAllUsers(), appendProgressMessage(),
    reportServerWithoutTalk(), navigateToServerSelectionAfterDelay()), simplified a fragile !!-chain into one safe-call expression,
    added a class-level overview doc.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

…ad of waiting up to 12h

CapabilitiesWorker always returned Result.success() even when the capabilities
fetch failed, so a single-user fetch (e.g. right after login) that failed was
never retried until the next periodic run, leaving user.capabilities null in
the meantime. A response with a null capabilities payload was also silently
swallowed without posting a failure event.

Now a targeted single-user fetch returns Result.retry() on failure, and
AccountVerificationActivity enqueues it as unique work with exponential
backoff and a network constraint so it retries automatically once
connectivity is back. Bulk (periodic/app-start chain) runs keep returning
success to avoid blocking dependent work.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Covers the success path plus every failure branch, including the
previously-silent case where the capabilities response has no ocs/data/
capabilities payload (now posts a CAPABILITIES_FETCH failure event instead
of doing nothing).

updateUser() is made package-visible (@VisibleForTesting) so it can be
called directly, bypassing doWork()'s Dagger field-injection. The Worker
instance itself is built via Robolectric + work-testing's
TestListenableWorkerBuilder, since Worker's constructor still requires a
real Context/WorkerParameters pair.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…es and split by retry contract

CapabilitiesWorker (Java, RxJava2) handled both a best-effort refresh across
every account and a single-account fetch that should retry with backoff,
picking its Result semantics based on whether an internal user id happened
to be present in the input data. That implicit branching made the class's
actual contract hard to read at a glance.

Split into:
- CapabilitiesFetcher: the actual fetch/persist logic (suspend, coroutines,
  a new NcApiCoroutines.getCapabilities endpoint), shared by both workers
  and testable directly without WorkManager/Robolectric scaffolding.
- CapabilitiesSyncWorker: best-effort refresh for every stored account, used
  by the app-start chain, the periodic sync, and the manual Settings
  refresh. Always returns Result.success() so it never blocks dependent
  work.
- CapabilitiesFetchWorker: single-account fetch used right after login,
  returns Result.retry() on failure so WorkManager retries with the
  backoff/constraints the caller already configures. If the target account
  no longer exists it now fails outright instead of silently falling back
  to syncing every account, since that fallback only existed to paper over
  the old shared code path.

CapabilitiesFetcherTest replaces CapabilitiesWorkerTest, testing the plain
CapabilitiesFetcher class directly with mocks instead of building a Worker
via Robolectric/work-testing, which is no longer needed.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
@mahibi mahibi self-assigned this Sep 20, 2026
…apabilities refresh fails

storeProfile() already persists the capabilities fetched during
findServerTalkApp()'s hasTalk check when it creates the account. The
CAPABILITIES_FETCH step that follows right after is a redundant re-fetch of
the same data, but a failure there called abortVerification(), which
deletes the account that was just correctly created - turning a transient
network hiccup on a throwaway re-check into full data loss.

Treat a failed capabilities refresh here the same as the sibling
PUSH_REGISTRATION/SIGNALING_SETTINGS failures: report it and continue the
login instead of aborting.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…round-tripping through EventBus

PROFILE_STORED and PUSH_REGISTRATION were posted to the global EventBus
just so AccountVerificationActivity's own onMessageEvent could react by
calling the next step - neither event ever crossed into a Worker, so the
bus added nothing but indirection and forced onMessageEvent to filter out
unrelated accounts' events (internalAccountId != eventStatus.userId).

storeProfile() now calls fetchAndStoreCapabilities() directly, and the
push-registration paths call a new onPushRegistrationFinished() directly.
CAPABILITIES_FETCH and SIGNALING_SETTINGS stay on EventBus since those are
genuinely posted from a background Worker - switching those to WorkInfo
observation was considered but would regress behavior (CapabilitiesFetchWorker
retries with backoff so its WorkInfo never reaches a terminal state
promptly, and SignalingSettingsWorker attaches no outputData to convey
success/failure).

PROFILE_STORED is now unused anywhere and is removed from EventStatus.EventType.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…o coroutines

Replaces the Observer/MaybeObserver callback chains for server status,
capabilities, profile fetch/store, push registration, and account
activation/deletion with sequential suspend functions launched via
lifecycleScope. UserManager's RxJava calls are bridged with
kotlinx-coroutines-rx2's await()/awaitSingle(), wrapped in
withContext(Dispatchers.IO) since UserManager doesn't specify its own
scheduler - the original code relied on the caller subscribing on IO or
already running on a background EventBus thread.

Adds getServerStatus() and getUserProfile() to NcApiCoroutines so the
Retrofit calls in this flow can be suspend directly; Retrofit's own
coroutine support means no explicit dispatcher wrapping is needed for
those.

Removes the disposables/dispose()/onDestroy() bookkeeping entirely, since
lifecycleScope coroutines are cancelled automatically when the Activity is
destroyed. Also drops the runOnUiThread wrapping throughout, since each
suspend function now defaults back to Dispatchers.Main after any IO hop.

WorkManager/LiveData usage and the EventBus-based CAPABILITIES_FETCH/
SIGNALING_SETTINGS handling are untouched - neither is RxJava.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Extracts repeated patterns into small helpers: getUser()/getAllUsers() for
the scattered withContext(Dispatchers.IO) { userManager... } blocks,
appendProgressMessage() for the repeated progress-text-append blocks,
reportServerWithoutTalk() for a byte-for-byte duplicated pair of branches
in findServerTalkApp(), and navigateToServerSelectionAfterDelay() for a
duplicated pair in abortVerification(). Also simplifies the scheme
handling in determineBaseUrlProtocol() and adds a class-level KDoc
summarizing the verification pipeline.

Simplifies findServerTalkApp()'s hasTalk check from four repeated !!
force-unwrap chains to one safe-call chain - this also means a null ocs/
data now falls through to "no Talk" and cleanly aborts instead of
crashing, a small but deliberate behavior improvement alongside the
readability cleanup.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
…count verification

storeProfile() already embeds the capabilities fetched moments earlier by
findServerTalkApp() into the new account (added in b6e9c9d, "Use already
fetched capabilities for user", to fix a null-capabilities crash right
after signup). The separate CapabilitiesFetchWorker step that followed was
the account's *original* sole source of capabilities before that fix, and
was left in place instead of being removed - it duplicated a request to
the same endpoint that had just succeeded, and its own failure was already
made non-fatal, since the account's capabilities were never actually at
risk.

storeProfile() now calls setupPushNotifications() directly. Removed the
CAPABILITIES_FETCH step, its EventBus case, CapabilitiesFetchWorker (its
only caller), and related now-dead code/imports.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
@mahibi mahibi changed the title Avoid null capabilities simplify AccountVerificationActivity + fix bugs Sep 20, 2026
@mahibi mahibi changed the title simplify AccountVerificationActivity + fix bugs simplify AccountVerificationActivity& CapabiliesWorker + fix bugs Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📱 QA build

Download app-qa-debug.apk
QR code Open the QR code for this download
Commit 66c8091
Version 6736
Available until 7 days after this build

The QA build installs alongside a released Nextcloud app, so you can keep
using your existing install while testing.

Downloading the file requires a GitHub account, so open this link on the
device you want to test on, or transfer the APK to it.

@mahibi
mahibi marked this pull request as ready for review September 20, 2026 15:35
@mahibi mahibi added the 3. to review Waiting for reviews label Sep 20, 2026
@mahibi mahibi added this to the 25.1.0 milestone Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants