Skip to content

ADFA-4960: resumable clone SEND (app-scoped) + clone notification - #312

Merged
luisguzman-adfa merged 4 commits into
mainfrom
feat/ADFA-4960-clone-send
Jul 31, 2026
Merged

ADFA-4960: resumable clone SEND (app-scoped) + clone notification#312
luisguzman-adfa merged 4 commits into
mainfrom
feat/ADFA-4960-clone-send

Conversation

@luisguzman-adfa

Copy link
Copy Markdown
Collaborator

The send rsync daemon already survives backgrounding/swipe (a foreground service keeps the process;
the daemon is a native child). The gaps were that send's live state lived in Fragment fields (a
recreated screen fell back to the fork) and clone used the generic WatchdogService notification
(→ MainActivity), which never returned to the Clone screen.

  • CloneSendSession: process-scoped holder (mirrors SyncProgressRepository for receive) with what the
    share screen redraws from — mode, tempPass (the QR secret, not recomputable), hostHasRootfs,
    librarySplit, shareAnyway. Set when the daemon comes up, cleared on stop. Process-scoped on purpose:
    it and the daemon share one lifetime, so a true kill leaves no false resume.
  • CloneFragment: re-binds on onCreateView to the live share; uses CloneShareService for keep-alive.
  • CloneShareService: clone-specific foreground keep-alive (own channel + id 8, holds the CPU/Wi-Fi
    locks) whose notification returns to LibraryActivity + nav_clone. Receive uses it too.
  • LibraryActivity: land on the Clone tab on a plain reopen when a SEND session is active.

Out of scope: true process death (receive doesn't survive it either). Pending a device pass.

…ation

The send rsync daemon already survives backgrounding/swipe (a foreground service keeps the process;
the daemon is a native child of it) — same profile as receive. The only gap was that send's live
state lived in Fragment fields, so a recreated CloneFragment fell back to the fork; and clone used the
generic WatchdogService notification (-> MainActivity), which doesn't return to the Clone screen.

- CloneSendSession: process-scoped holder (mirrors SyncProgressRepository for receive) carrying what
  the share screen redraws from — mode, tempPass (the QR secret, not recomputable), hostHasRootfs,
  librarySplit, shareAnyway. Set when the daemon comes up, cleared on stop. Process-scoped on purpose:
  it and the daemon share one lifetime, so a true process kill leaves no false resume.
- CloneFragment: re-binds on onCreateView when a send session is active (land on the share screen,
  skip a re-start); writes/clears the session; and now uses CloneShareService for keep-alive.
- CloneShareService: clone-specific foreground keep-alive (own channel + id 8, holds the CPU/Wi-Fi
  locks) whose notification content intent returns to LibraryActivity + nav_clone. Receive uses it too.
- LibraryActivity: land on the Clone tab on a plain reopen when a SEND session is active (already did
  so for receive).

True process death is out of scope (receive doesn't survive it either). Wants a device pass.
#1 CloneShareService: on a START_STICKY null-intent restart with no active clone (session + daemon
died with the process), stopSelf instead of resurrecting a stale 'transferring' notification / locks.
#2 CloneSendSession.begin: set the volatile active flag last so a reader that sees it also sees the
rest of the fields.
…ot gate (device fixes)

Device testing revealed two problems the earlier analysis missed:

1) Swipe cut the transfer. Root cause: SyncStateViewModel.onCleared() called transport.stop() (it is
   ViewModel-scoped, ADFA-4492). Swiping the app finishes the Activity -> the ViewModel is cleared ->
   the daemon/pull is killed, even though CloneShareService keeps the PROCESS alive. This hit send AND
   receive. Fix: the transport is now process-scoped (static), and onCleared() does NOT tear it down
   while a clone is in flight (CloneSendSession.isActive() || SyncProgressRepository.isActive()) — only
   when nothing is running. The native rsync child now stays with the process the service keeps alive.

2) Reopening during a clone (e.g. tapping the notification) sat on the boot gate 'loading forever'.
   LibraryActivity waits behind the gate for the server, but a clone intentionally stops it. Fix: when
   a deep-env op holds the lock (EnvironmentLock.ownerHeld), lift the gate immediately and show the UI
   we routed to (the Clone tab); the op boots the server when it finishes, never the gate.

Note: a device whose OEM/phantom-process killer reaps the app or the rsync child on swipe is a deeper
issue beyond this fix, but the self-inflicted onCleared kill is gone. Wants a re-test.
#1 SyncStateViewModel.getTransport(): double-checked lazy init on a volatile field, so the now
process-global transport is never built twice by the main thread + a probe's IO thread (a stray
RsyncManager/rsync child a later stop() couldn't reach).
#2 SyncFragment.onDestroyView() (old UI): don't stop the shared transport / drop the network binding
while a clone is in flight — that old-UI teardown would otherwise kill the clone daemon and strand its
lock + keep-alive. Follow-ups noted in the PR: daemon liveness check on re-bind; CONFIRM-phase network
binding on onCleared.
@luisguzman-adfa
luisguzman-adfa merged commit d5c157c into main Jul 31, 2026
3 checks passed
@luisguzman-adfa
luisguzman-adfa deleted the feat/ADFA-4960-clone-send branch July 31, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant