Commit 8a70c7e
committed
feat: make transfers operation-scoped and prepare preview 0.2.0
This update substantially reworks how Sync360 discovers nearby devices,
starts transfers, tracks incoming operations, handles cancellation, and
coordinates platform-specific file sockets.
The main goal is to make each transfer an explicit, identifiable operation
whose state can be followed consistently from the initial offer through
acceptance, transfer, completion, failure, or cancellation.
This also simplifies the nearby-device sending experience, improves adaptive
navigation, fills the missing receiving-text UI state, aligns the Android,
desktop, and iOS transfer implementations, updates the public documentation,
and bumps the preview version to 0.2.0.
Nearby-device sending
---------------------
Change the send screen from a select-then-send flow to a direct-send flow.
Previously, the user selected a nearby device and then pressed a separate
Send button. The send screen maintained selected-device state and derived
additional button state and labels from that selection.
Now:
- entering text or adding files prepares the selected content for sending
- each nearby-device row becomes the action that starts the transfer
- clicking an enabled device immediately sends the prepared content to it
- device rows stay disabled until the current text or file selection is ready
- each row explains the required or available action, such as:
- "Enter text to send"
- "Click me to send text"
- "Add files to send"
- "Click me to send files"
- the separate bottom Send button is removed
- selected-device state, selected-device lookup, radio-button presentation,
and selectable-group semantics are removed
Keep the latest full nearby-device domain objects in the send view model while
continuing to expose the smaller display model required by the UI. This allows
a clicked device ID to be resolved to the connection information required by
the outgoing transfer controller without duplicating selection state.
Improve the scanning card so it represents the actual discovery lifecycle.
The card now distinguishes idle, starting, running, and stopping discovery,
shows progress while discovery is starting or stopping, and only exposes the
rescan action when that action is currently valid.
Rename the previous empty-device card to the nearby-device scanning card so
its name reflects its broader responsibility.
Operation-scoped outgoing transfers
-----------------------------------
Assign a new UUID to every outgoing text or file operation.
Pass the same operation ID through the entire transfer lifecycle:
- send view-model operation tracking
- text or file offer
- accepted text payload
- file TCP connection
- cancellation request
- receiver-side state validation
Replace the previous send-job-only tracking with an active-send record that
contains both the operation ID and target device. This gives cancellation
enough information to stop the local work and notify the correct receiver.
Clear active send references only when the completing job still belongs to
the same operation. This prevents an older job's completion callback from
accidentally clearing a newer transfer.
Continue rejecting invalid send attempts, including blank text, empty file
lists, unknown file sizes, and attempts made while another send operation is
active.
Limit surfaced error text to a reasonable length so unexpected platform or
network exception messages cannot overwhelm the UI.
Cancellation improvements
-------------------------
Make cancellation a coordinated operation instead of only cancelling the
local coroutine.
When the sender cancels an active transfer:
- capture the active operation and target device
- clear the active operation from the send view model
- cancel the local send coroutine
- close the sender's current file socket when applicable
- send an independent cancellation request to the receiver
- identify the cancellation using both operation ID and sender device ID
- move the sender UI to the cancelled state
Run the remote cancellation request independently from the cancelled transfer
job so cancelling the original job does not also prevent the cancellation
notification from being sent.
Add serialized cancellation request and response models and introduce the
server endpoint:
/sync360/operation/cancel
Give cancellation requests a short request timeout so cleanup does not remain
blocked behind an unavailable receiver.
On the receiving side, cancel only when the supplied operation ID and sender
device ID match the currently active offer, accepted text wait, accepted file
wait, or file transfer. A stale cancellation can therefore no longer clear an
unrelated or newer operation.
If a matching file operation is cancelled, also close the platform receiver's
active or pending file socket. This allows a transfer blocked in connection
verification or file reads to stop promptly instead of waiting only for a
network timeout.
Incoming server state machine
-----------------------------
Rework IncomingServerRequestsController around ClientServerState as the
authoritative description of the current incoming operation.
The incoming text lifecycle is now:
Idle
-> TextOffer
-> WaitingForText
-> TextReceived
The incoming file lifecycle is now:
Idle
-> FileOffer
-> WaitingForFiles
-> ReceivingFiles
-> FilesReceived
Declined, failed, expired, or cancelled operations return to Idle as
appropriate.
Store the complete text or file offer request in its associated server state.
The operation ID, sender identity, sender name, text preview, and offered-file
metadata can now be derived from the active state instead of being duplicated
in parallel controller variables.
Replace the previous incoming-operation-kind and active-operation bookkeeping
with state matching based on the actual ClientServerState.
Keep the pending user-decision deferred as controller-owned synchronization
plumbing. It is intentionally not moved into ClientServerState because it
represents how a suspended server request receives its answer, not what should
be presented to the UI.
Protect incoming state transitions and pending decisions with a mutex. This
serializes admission, acceptance, decline, cancellation, timeout, progress,
completion, and clearing decisions around one active incoming operation.
Run controller work in a supervisor-backed scope so a failure in one timeout
or cleanup task does not automatically cancel unrelated controller work.
Only admit a new text or file offer while the incoming server is idle. This
preserves the single-active-incoming-operation model and gives concurrent
senders a predictable busy or declined result.
Give the user up to 50 seconds to accept or decline an incoming offer. If the
decision does not arrive in time, expire the matching offer and restore the
idle state.
After an offer is accepted, apply a separate 30-second wait for the accepted
sender to continue the protocol. Cancel that timer automatically when the
state leaves the accepted waiting state.
Perform non-cancellable cleanup if the HTTP coroutine waiting for a user
decision is cancelled, then rethrow the original cancellation exception.
This preserves structured coroutine cancellation while still preventing a
stale offer from remaining visible.
Make controller actions operation-aware:
- decisions apply only to the currently displayed offer
- text payloads apply only to the matching WaitingForText state
- file preparation applies only to the matching WaitingForFiles state
- file progress applies only to the matching ReceivingFiles state
- file completion applies only to the matching ReceivingFiles state
- cancellation applies only to the matching active operation
- stale callbacks cannot mutate a newer operation
Restrict clearState to idle or terminal presentation states. Clearing the UI
can no longer silently interrupt an active offer or transfer.
Waiting-for-text receive state
------------------------------
Add the missing state between accepting a text offer and receiving its text
payload.
Previously, accepting a text offer temporarily returned the receiver UI to
idle before the text payload arrived. On a fast local network this appeared
as a brief blink, but it also meant the UI did not represent what the server
was actually doing.
The receiver now enters WaitingForText immediately after accepting the offer.
Add a dedicated waiting-for-text UI that:
- retains the sender identity
- displays a receiving-text title
- explains that Sync360 is waiting for the text payload
- shows an active loading indicator
Map WaitingForText from the server state into the receiver presentation state
and render it from the receive screen and root navigation title.
Map WaitingForFiles into the existing receiving-files presentation with zero
completed files and waiting progress. This keeps the receiver presentation
continuous between accepting a file offer and receiving the first file byte.
Rename terminal server states to TextReceived and FilesReceived for clearer,
consistent wording.
Add a CANCELLED user decision so server-side cancellation can complete a
request currently suspended while awaiting the user's response.
HTTP transfer protocol
----------------------
Extend text offers, text payloads, and file offers with operation identity.
Text transfers now use the same operation ID for both:
- the initial text offer
- the full text payload sent after acceptance
Include the sender device ID in the full text request so the receiver can
validate both parts of the active operation before accepting the payload.
Increase the text offer preview limit from 180 to 200 characters.
Rename the file-offer collection from files to offeredFiles so the model
clearly describes metadata being offered before any bytes have been accepted.
Pass an externally created operation ID into the outgoing controller instead
of allowing individual protocol stages to create unrelated identities.
Rename the outgoing file-offer operation to sendFilesToDevice and keep the
accepted-offer-to-TCP-transfer sequence together.
Check the success value in the text transfer response instead of assuming that
any HTTP response means the text was accepted.
Report a text rejection as a text-transfer error and correct the previous
timeout path that could incorrectly surface a file-offer exception while
sending text.
Continue rethrowing coroutine cancellation exceptions rather than converting
them into ordinary transfer failures.
Retain multi-address connection fallback so Sync360 can try the available
addresses for a nearby device instead of failing after only the first address.
Server route coordination
-------------------------
Move incoming-operation admission, state transitions, timeout ownership,
decision handling, validation, cancellation, and completion into the incoming
controller.
Keep HTTP routes focused on:
- validating the request shape
- calling the appropriate controller operation
- returning the matching protocol response
A text offer is now accepted only when the controller successfully admits the
offer and the user accepts it.
A full text payload is accepted only when its operation ID and sender device
ID match the active WaitingForText state.
A file offer is accepted only after:
- the controller admits the offer
- the user accepts it
- the platform file receiver successfully prepares the matching expected
transfer
File progress and completion callbacks carry the operation identity back into
the incoming controller, preventing callbacks from a previous socket from
changing the state of a newer transfer.
Remove route-local state construction and duplicated timeout handling now
owned by the controller state machine.
File TCP framing
----------------
Make the operation ID part of the file-transfer TCP protocol.
Every Android, desktop, and iOS file sender now writes the operation UUID as
exactly 16 raw bytes at the beginning of the TCP connection, before any file
frames.
The UUID is not length-prefixed. The receiver reads exactly 16 bytes and
verifies them against the operation accepted over HTTP.
After UUID verification, the existing file framing continues to transfer:
- file index
- file size
- exact file bytes
- final batch acknowledgement
Continue validating file indexes and declared file sizes before accepting file
contents.
Reject empty file batches consistently instead of allowing platform
implementations to return an ambiguous unsuccessful result.
Keep the final acknowledgement for the complete batch rather than treating a
socket connection alone as a successful transfer.
Android and desktop file sockets
--------------------------------
Update the Android and JVM senders to prepend the raw operation UUID and keep
the active socket in an atomic reference.
Closing the active socket remains the immediate local cancellation mechanism.
This allows a blocked connection or write to terminate when the user cancels.
Replace the receiver's collection of loosely related mutable fields with one
ExpectedTransfer object containing:
- the accepted file offer
- progress and completion callbacks
- the currently connected or verifying socket
- whether the expected transfer has been claimed
- the first-sender timeout job
Protect expected-transfer lifecycle changes with synchronization.
Store an accepted socket before UUID verification. This allows timeout or
remote cancellation to close a connection even if it stalls before sending
its operation ID.
Claim the expected transfer only after the incoming UUID matches. A socket
with a wrong or stale UUID is closed without consuming the valid accepted
offer, allowing the correct sender to connect afterward.
Make timeout and cleanup identity-safe. A delayed timeout or completion from
an older operation cannot clear a newer expected transfer.
Invoke completion callbacks outside the synchronization boundary so
controller work cannot deadlock the socket lifecycle lock.
Cancel only the expected transfer whose operation ID matches the cancellation
request.
Preserve successfully completed files while allowing the storage layer to
remove or abandon only an unfinished temporary file.
iOS file socket lifecycle
-------------------------
Apply the same operation-scoped framing and expected-transfer model to iOS.
Use NSLock to protect sender socket references and receiver expected-transfer
state while retaining explicit native lifecycle handling.
Read the exact 16-byte UUID prefix through the Ktor channel before allowing
the socket to claim the expected transfer.
Retain explicit NSInputStream cleanup and security-scoped resource access for
selected files. These native lifecycle responsibilities remain visible rather
than being hidden behind abstractions that could release resources too early.
Close sender and receiver sockets in their finalization paths and allow
operation-specific cancellation to close a connection that is still being
verified or transferred.
Keep callbacks outside the native lock and prevent stale timeout or
completion work from clearing a newer transfer.
Navigation and adaptive layout
------------------------------
Make navigation transitions explicit in NavigationViewModel.
Replace direct back-stack manipulation from the UI with dedicated operations
for:
- navigating to Send
- navigating to Receive
- navigating to Settings
- going back
- changing between compact and two-pane layouts
Start with Send as the only initial destination.
When the app enters the two-pane layout:
- keep Send as the primary destination
- add Receive as the secondary destination
- temporarily remove Settings from the visible pane stack
When the app returns to compact layout:
- remove the two-pane Receive entry
- restore Settings when it was open before the layout change
- keep compact navigation deterministic
Represent two-pane entry roles with a PaneRole type instead of unrelated
Boolean metadata.
Avoid passing previous navigation entries into the two-pane scene so stale
back-stack entries do not interfere with the current Send and Receive panes.
Continue using the medium-width breakpoint and equal pane weighting.
Automatically navigate to Receive when a new incoming offer becomes visible.
Add a reusable Back icon and use it for Settings navigation instead of
presenting Settings dismissal as a close action.
Screen-awake behavior
---------------------
Keep the device screen awake while a send or receive operation is active.
This includes incoming offers, accepted waiting states, transfers, and terminal
states that are still being presented to the user.
Release the screen-awake request after both transfer sides return to idle.
This reduces the chance that a transfer, user decision, or completion screen is
interrupted by the device sleeping while preserving normal sleep behavior when
Sync360 is inactive.
Repair availability
-------------------
Continue allowing the repair action only when:
- the sender is idle
- the receiver is idle
- discovery is in a stable state
- registration is in a stable state
This prevents repair from resetting networking components while an operation
is being offered, accepted, transferred, or cancelled.
Preview 0.2.0 version update
----------------------------
Bump the application preview version from 0.1.0 to 0.2.0 across supported
package targets.
Update:
- Android versionCode from 1 to 2
- Android versionName from 0.1.0 to 0.2.0
- desktop packageVersion to 0.2.0
- iOS CURRENT_PROJECT_VERSION from 1 to 2
- iOS MARKETING_VERSION from 0.1.0 to 0.2.0
Preserve the existing desktop upgrade UUID so installers continue to identify
0.2.0 as an upgrade of the same application.
Documentation and branding
--------------------------
Update the README to use the current Sync360 application icon while preserving
the existing logo size and layout.
Refresh the README, architecture documentation, development notes, roadmap,
screenshot guidance, changelog, and internal project context to match the
current source implementation.
Document:
- operation-scoped text and file transfers
- sender and receiver cancellation behavior
- the incoming server state machine
- WaitingForText and WaitingForFiles
- exact raw 16-byte UUID TCP framing
- offer and accepted-transfer timeouts
- current adaptive navigation behavior
- current platform-specific transfer responsibilities
- the 0.2.0 preview version
Keep the security description explicit: operation IDs provide transfer
correlation and stale-operation protection, but they are not authentication,
encryption, integrity hashes, or protection against a malicious device on the
same network.
Known limitations and follow-up work
------------------------------------
Document known limitations instead of presenting the preview as fully solved:
- Android 17 local-network enforcement remains a future compatibility risk
because target SDK 37 does not yet declare ACCESS_LOCAL_NETWORK
- Android 13 and earlier can still encounter overlapping discovery and
registration behavior in the legacy networking path
- a narrow timing race can still exist when user acceptance and remote
cancellation produce their HTTP responses at almost the same moment
- Windows firewall configuration can still prevent discovery or transfer
connections
- retired native discovery arenas can remain allocated longer than desired
- per-interface discovery TTL removal remains follow-up work
- transfer traffic remains unencrypted on the local network
These limitations are tracked separately and do not change the operation
identity, cancellation, state-machine, navigation, or user-experience
improvements included here.1 parent 56c00cd commit 8a70c7e
44 files changed
Lines changed: 1445 additions & 612 deletions
File tree
- androidApp
- desktopApp
- docs
- iosApp/Configuration
- screenshots
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
57 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
58 | 63 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
81 | 83 | | |
| 84 | + | |
82 | 85 | | |
83 | 86 | | |
84 | 87 | | |
| |||
112 | 115 | | |
113 | 116 | | |
114 | 117 | | |
| 118 | + | |
115 | 119 | | |
116 | 120 | | |
117 | 121 | | |
118 | 122 | | |
119 | | - | |
| 123 | + | |
120 | 124 | | |
121 | 125 | | |
122 | 126 | | |
| |||
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
134 | | - | |
| 138 | + | |
135 | 139 | | |
136 | 140 | | |
137 | 141 | | |
| |||
247 | 251 | | |
248 | 252 | | |
249 | 253 | | |
250 | | - | |
| 254 | + | |
251 | 255 | | |
252 | 256 | | |
253 | 257 | | |
| |||
257 | 261 | | |
258 | 262 | | |
259 | 263 | | |
260 | | - | |
| 264 | + | |
| 265 | + | |
261 | 266 | | |
| 267 | + | |
262 | 268 | | |
263 | 269 | | |
264 | 270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
40 | | - | |
| 39 | + | |
| 40 | + | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| 58 | + | |
57 | 59 | | |
58 | 60 | | |
59 | 61 | | |
| |||
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
| 75 | + | |
| 76 | + | |
73 | 77 | | |
74 | 78 | | |
75 | 79 | | |
76 | 80 | | |
77 | | - | |
| 81 | + | |
78 | 82 | | |
79 | 83 | | |
80 | 84 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
| 78 | + | |
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| 96 | + | |
| 97 | + | |
96 | 98 | | |
97 | 99 | | |
98 | 100 | | |
| |||
103 | 105 | | |
104 | 106 | | |
105 | 107 | | |
| 108 | + | |
106 | 109 | | |
107 | 110 | | |
108 | | - | |
| 111 | + | |
109 | 112 | | |
110 | 113 | | |
111 | 114 | | |
112 | 115 | | |
113 | 116 | | |
114 | 117 | | |
115 | 118 | | |
| 119 | + | |
116 | 120 | | |
117 | 121 | | |
118 | 122 | | |
| |||
122 | 126 | | |
123 | 127 | | |
124 | 128 | | |
125 | | - | |
| 129 | + | |
126 | 130 | | |
127 | 131 | | |
128 | 132 | | |
| |||
146 | 150 | | |
147 | 151 | | |
148 | 152 | | |
| 153 | + | |
| 154 | + | |
149 | 155 | | |
150 | 156 | | |
151 | | - | |
| 157 | + | |
| 158 | + | |
152 | 159 | | |
153 | 160 | | |
0 commit comments