Skip to content

Commit 8a70c7e

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1717
- Text offers, receiver Accept/Decline, text transfer, Copy, and Clear.
1818
- Android and Desktop multiple-file selection and metadata offers.
1919
- Raw TCP file transfer using one persistent connection per accepted batch.
20-
- Sequential file framing with index/size validation and one final batch result containing receiver success and the completed-file count.
20+
- Operation-bound file framing with operation ID, index, and size validation plus one final batch result containing receiver success and the completed-file count.
2121
- Android Downloads writing through pending `MediaStore` entries.
2222
- Desktop Downloads writing through temporary `.part` files and collision-safe final names.
23-
- Unified send operation states and best-effort cancellation.
23+
- Unified send operation states and explicit operation-scoped cancellation, with timeout fallbacks for lost communication.
2424
- Shared transfer buffer/timeout constants, currently using a 512 KiB payload buffer.
2525
- Compose Desktop startup, platform DI implementations, native file dialog, clipboard, and Downloads actions.
2626
- Navigation 3 adaptive 50/50 Send/Receive scene for wider windows.
2727
- Application-lifetime network startup and state-driven connection repair.
2828
- Enabled iOS device and Apple-silicon Simulator targets with native Bonjour discovery, document selection, clipboard, Files-visible storage, and streamed TCP transfer implementations.
2929
- Added an iOS-only GitHub Actions workflow for an unsigned Simulator app and optional development-signed iPhone IPA.
30-
- Prepared version `0.1.0` across Android, Desktop, and iOS; added private Android release signing configuration and a permanent Windows MSI upgrade identity.
30+
- Prepared version `0.2.0` across Android, Desktop, and iOS; retained private Android release signing configuration and the permanent Windows MSI upgrade identity.
3131
- Public architecture, development, roadmap, security, privacy, and contribution documentation.
3232

3333
### Changed
3434

35+
- Made incoming transfer state the source of truth for offer type, operation identity, and acceptance phase; accepted text now shows a waiting state until its matching payload arrives.
3536
- Replaced the old generated sync implementation with a smaller, manually understood flow.
3637
- Separated Ktor HTTP offer/control messages from raw TCP file bytes.
3738
- Reused one TCP connection for the complete accepted multi-file batch instead of opening one connection per file.
@@ -54,5 +55,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5455
- No authentication, encryption, transfer/session token, or cryptographic integrity verification.
5556
- No speed, ETA, retry, pause/resume, or interrupted-transfer recovery; transfer progress currently shows batch-wide whole-byte percentage.
5657
- Foreground/background and network-change lifecycle handling are incomplete.
57-
- Desktop support needs broader operating-system, adapter, firewall, and router validation.
58+
- The target-SDK-37 Android build does not yet declare or request Android 17's `ACCESS_LOCAL_NETWORK` runtime permission, so LAN discovery and transfer are blocked by default there.
59+
- Android 13 starts legacy NSD resolves immediately; overlapping discoveries can fail with an already-active resolve and are not currently retried.
60+
- A narrow Accept/Cancel race can let an offer response report acceptance after the matching receiver state was cancelled.
61+
- Desktop support needs broader operating-system, adapter, firewall, and router validation. Windows inbound sharing also depends on the user or administrator allowing Sync360 through Windows Firewall.
62+
- Windows retains completed native callback arenas for safety instead of closing them later, so repeated discovery repairs can slowly retain native memory; a service removal on one interface can also temporarily remove that service's results from other interfaces.
5863
- Automated transfer coverage is minimal; iOS physical-device discovery and transfer are unverified.

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div align="center">
2-
<img src="screenshots/sync360-icon.png" width="128" alt="Sync360 app icon" />
2+
<img src="shared/src/commonMain/composeResources/drawable/app_icon.png" width="128" alt="Sync360 app icon" />
33

44
# Sync360
55

@@ -78,7 +78,10 @@ In an initial Windows 11 Ethernet test, the native Windows DNS-SD backend discov
7878
- Broader IPv6 transfer validation and better address preference/selection.
7979
- Retry, pause/resume, and interrupted-transfer recovery.
8080
- Automated transfer coverage and broader device/router testing.
81+
- Android 17 local-network permission declaration, runtime request, and permission-aware network startup. The current target-SDK-37 build does not yet provide these, so LAN discovery and transfer are blocked by default on Android 17.
82+
- Serialize legacy Android 13 NSD resolution so several devices discovered together are not lost when another resolve is already active.
8183
- Broader Desktop validation across Windows, macOS, Linux, routers, firewalls, VPNs, and machines with multiple network adapters.
84+
- Better Windows first-run firewall guidance; inbound sharing depends on the user or administrator allowing Sync360 through Windows Firewall.
8285
- Desktop packaging and release testing.
8386
- Physical iOS device testing for local-network permission, discovery, text/file transfer, cancellation, and Files behavior.
8487
- Public iOS packaging, signing, and distribution.
@@ -112,11 +115,12 @@ SendScreen
112115
-> OutgoingRequestsController
113116
-> POST /sync360/text/offer
114117
-> receiver Accept/Decline
118+
-> accepted receiver waits for the matching text payload
115119
-> POST /sync360/text/transfer
116120
-> ReceiveScreen shows the text
117121
```
118122

119-
The sender shares a preview and character count first. The full text is posted only after the receiver accepts.
123+
The sender shares a preview and character count first. After acceptance, the receiver remains in a waiting-for-text state until the matching full text arrives. One operation ID ties the offer, accepted payload, and any explicit cancellation to the same sender operation.
120124

121125
### File path
122126

@@ -131,7 +135,7 @@ Platform file picker
131135
-> receiver returns final success and completed-file count
132136
```
133137

134-
One TCP socket is opened for the complete accepted batch. Each file begins with its index and promised byte count, followed by exactly that many bytes. The receiver checks the index and size against the accepted offer before saving each file. The sender writes every file sequentially, flushes once after the complete batch, then reads one final success flag and completed-file count from the receiver. The count increases only after the platform Downloads writer successfully returns. The current shared payload buffer is 512 KiB; exact byte counts define file boundaries, so correctness does not depend on `flush()` calls or matching sender and receiver read chunks.
138+
One TCP socket is opened for the complete accepted batch. It begins with the operation ID as 16 raw UUID bytes; each file then begins with its index and promised byte count, followed by exactly that many bytes. The receiver checks the operation ID, index, and size before saving. The sender writes every file sequentially, flushes once after the complete batch, then reads one final success flag and completed-file count from the receiver. The count increases only after the platform Downloads writer successfully returns. The current shared payload buffer is 512 KiB; exact byte counts define file boundaries, so correctness does not depend on `flush()` calls or matching sender and receiver read chunks.
135139

136140
Files are sent sequentially. If a later file fails, files that were already completed stay in Downloads; the incomplete current file is cleaned up. Android uses a pending `MediaStore` entry and resolves its MIME type from the filename extension so duplicate names remain in the form `file (1).ext`. Desktop writes a temporary `.part` file before moving a completed file into place without overwriting an existing name.
137141

@@ -247,7 +251,7 @@ Reload is available only after the current discovery window has stopped while se
247251

248252
Sync360 is **not secure for untrusted networks yet**.
249253

250-
The current Android/Desktop implementation uses cleartext local HTTP and raw TCP. It does not authenticate the sender, encrypt content, bind file sockets to an approved session token, or verify file integrity with a cryptographic hash. Receiver approval exists in the UI, but it is not a complete security boundary.
254+
The current implementation uses cleartext local HTTP and raw TCP. Operation IDs correlate offers, cancellation, accepted text, and file sockets for correctness, but they are not secret or authenticated. Sync360 does not yet authenticate the sender, encrypt content, or verify file integrity with a cryptographic hash. Receiver approval exists in the UI, but it is not a complete security boundary.
251255

252256
Use the current app only for development and testing on private networks you control. Please report security-sensitive findings according to [SECURITY.md](SECURITY.md), not in a public issue.
253257

@@ -257,8 +261,10 @@ Use the current app only for development and testing on private networks you con
257261

258262
- Improve active-transfer feedback around the current byte percentage.
259263
- Add integrity verification.
260-
- Improve cancellation and failure reporting.
264+
- Test cancellation and failure reporting across more network-loss and transfer stages.
265+
- Close the narrow Accept/Cancel timing gap so an offer cannot report acceptance after its receiver state has already been cancelled.
261266
- Strengthen lifecycle behavior and local-network reliability.
267+
- Add Android 17 local-network permission handling and serialize Android 13 legacy NSD resolves.
262268
- Validate Desktop discovery and transfer across more operating systems, network adapters, routers, and firewall configurations.
263269
- Design session validation, authentication, and encryption deliberately.
264270

androidApp/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ android {
3636
applicationId = "com.liftley.sync360"
3737
minSdk = libs.versions.android.minSdk.get().toInt()
3838
targetSdk = libs.versions.android.targetSdk.get().toInt()
39-
versionCode = 1
40-
versionName = "0.1.0"
39+
versionCode = 2
40+
versionName = "0.2.0"
4141
}
4242

4343
buildFeatures {

context.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ Ktor HTTP is the control plane:
4848
POST /sync360/text/offer
4949
POST /sync360/text/transfer
5050
POST /sync360/file/offer
51+
POST /sync360/operation/cancel
5152
```
5253

5354
Raw TCP is the file data plane:
5455

5556
```text
5657
one connection per accepted batch
58+
-> accepted operation ID as 16 raw UUID bytes
5759
-> repeat for each file:
5860
-> file index
5961
-> promised byte count
@@ -70,11 +72,13 @@ Current shared transfer constants use a 512 KiB payload buffer, 5-second connect
7072
- Automatic registration repair after network/address changes.
7173
- Foreground/background lifecycle support.
7274
- Broader Desktop adapter, firewall, router, and operating-system validation.
75+
- Android 17 local-network permission-aware startup and serialized Android 13 legacy NSD resolution.
76+
- Closing the narrow shared Accept/Cancel response race.
7377
- Session validation, authentication, encryption, and integrity verification.
7478

7579
## Important limitations
7680

77-
Sync360 currently uses cleartext local HTTP and raw TCP. It has receiver approval but no authentication, encryption, transfer token, or checksum. Use development builds only on private networks you control.
81+
Sync360 currently uses cleartext local HTTP and raw TCP. Operation IDs correlate protocol messages and file sockets but do not authenticate a peer. The app has receiver approval but no authentication, encryption, or checksum. The current target-SDK-37 Android build also lacks Android 17's required local-network runtime-permission flow. Windows receiving depends on Windows Firewall allowing the application. Use development builds only on private networks you control.
7882

7983
For detailed and current information, read:
8084

desktopApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ compose.desktop {
3737
nativeDistributions {
3838
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
3939
packageName = "Sync360"
40-
packageVersion = "0.1.0"
40+
packageVersion = "0.2.0"
4141
appResourcesRootDir.set(
4242
project.layout.projectDirectory.dir("packaging/app-resources")
4343
)

docs/ARCHITECTURE.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ ViewModels launch UI-facing work. They do not implement platform APIs or socket
7575

7676
- `NetworkServicesController` starts the HTTP server, file receiver, and discovery/registration once for the application lifetime. It also coordinates timed discovery stop, discovery restart, and full connection repair.
7777
- `OutgoingRequestsController` creates offers, calls the Ktor client, and starts accepted file transfers.
78-
- `IncomingServerRequestsController` exposes incoming offers and receiver decisions to the HTTP server and Receive UI.
78+
- `IncomingServerRequestsController` serializes Accept/Decline/Cancel races and uses `ClientServerState` as the source of truth for the active incoming operation. Text follows `TextOffer -> WaitingForText -> TextReceived`; files follow `FileOffer -> WaitingForFiles -> ReceivingFiles -> FilesReceived`. Active states retain their accepted request, so operation type, sender identity, operation ID, and acceptance phase are derived from state instead of duplicated in a second operation model.
7979

8080
### Discovery
8181

@@ -93,6 +93,8 @@ Connection repair waits until both operations are stable, then stops discovery a
9393

9494
Windows calls `DnsServiceBrowse`, `DnsServiceResolve`, `DnsServiceRegister`, and `DnsServiceDeRegister` through the JDK Foreign Function and Memory API. Browse and registration use interface index `0`, which delegates all-interface IPv4/IPv6 handling to Windows. Native registration and deregistration callbacks drive `RegistrationStatus`; browse cancellation drives the final transition back to `DiscoveryStatus.Idle`. Browse callbacks start resolution for added PTR records and remove devices reported with a zero TTL. Resolved TXT properties and IPv4/IPv6 addresses are converted into the same shared `NearbyDevice` model used by Android.
9595

96+
The Windows implementation keeps native request memory alive after terminal callbacks because a callback is still unwinding when Kotlin receives it. Those retired arenas are not yet closed later, so repeated repair cycles can retain small native allocations. Resolved results are keyed by service name and interface, but a TTL-zero browse removal currently clears every interface result for that service name.
97+
9698
The macOS/Linux JmDNS fallback starts on eligible IPv4 and IPv6 addresses from every active, multicast-capable, non-loopback, non-virtual LAN interface. Windows DNS-SD and the fallback still need broader validation with VPN, WSL, Docker, virtual-machine, Ethernet, and Wi-Fi adapters.
9799

98100
## Control plane: Ktor HTTP
@@ -103,16 +105,18 @@ Ktor carries offers, decisions, metadata, and text:
103105
POST /sync360/text/offer
104106
POST /sync360/text/transfer
105107
POST /sync360/file/offer
108+
POST /sync360/operation/cancel
106109
```
107110

108-
An offer waits up to 55 seconds for the receiver's decision. The shared flow uses `FileOfferRequest` directly for the accepted metadata; file contents still remain in platform file readers and are not placed in the HTTP request.
111+
An offer waits up to 50 seconds for the receiver's decision. After acceptance, the controller derives a 30-second payload-preparation timeout from `WaitingForText` or `WaitingForFiles`; leaving either state automatically cancels that timer. A random operation ID correlates the offer, accepted payload, explicit cancellation, and file connection. Cancellation succeeds only when both the operation ID and sender device ID match the active state. The timeouts remain fallbacks for crashes and lost network communication. The shared flow uses `FileOfferRequest` directly for the accepted metadata; file contents still remain in platform file readers and are not placed in the HTTP request.
109112

110113
## File data plane: raw TCP
111114

112115
Accepted file bytes use a separate raw TCP connection:
113116

114117
```text
115118
one connection for the accepted batch
119+
-> operation ID: 16 raw UUID bytes
116120
-> repeat for each accepted file:
117121
-> file index: Int
118122
-> promised file size: Long
@@ -122,7 +126,7 @@ one connection for the accepted batch
122126
-> completed-file count: Int
123127
```
124128

125-
Files remain sequential. The receiver verifies each index and size directly against the matching file in the accepted offer before saving. It increments the completed-file count only after the platform Downloads writer returns successfully. After every file has been processed, the receiver sends one final success flag and completed count. If processing fails, it attempts to send `false` with the number of files that were fully saved.
129+
Files remain sequential. The receiver first verifies that the socket operation ID matches the accepted offer, then verifies each index and size directly against the matching file before saving. It increments the completed-file count only after the platform Downloads writer returns successfully. After every file has been processed, the receiver sends one final success flag and completed count. If processing fails, it attempts to send `false` with the number of files that were fully saved.
126130

127131
`FileTransferConstants` currently provides:
128132

@@ -146,8 +150,11 @@ Previously completed files remain when a later file in the same batch fails.
146150
- No authentication, encryption, session token, or cryptographic integrity check.
147151
- No retry, pause/resume, or interrupted-transfer recovery.
148152
- Foreground/background and automatic network-change lifecycle handling are not complete.
153+
- Android 17 local-network permission handling is not implemented even though the app targets SDK 37; Android 13 legacy NSD resolves are not serialized or retried after an already-active failure.
154+
- Accepting and cancelling in the narrow interval before the suspended offer handler is resumed can produce an accepted offer response after receiver state has already returned to idle.
149155
- Receiver failures do not yet provide rich error details.
150156
- HTTP and file-transfer senders retry distinct advertised addresses after connection failures; broader address preference and scoped IPv6 validation still need work.
151-
- Desktop interface selection and firewall behavior need broader validation.
157+
- Desktop interface selection and firewall behavior need broader validation. Windows inbound transfers require an application allow rule or user-approved firewall prompt.
158+
- Repeated Windows repair cycles retain completed native callback arenas, and removing a service from one interface can temporarily clear the same service resolved through another interface.
152159
- Automated transfer coverage is minimal.
153160
- iOS source targets and implementations are enabled, but physical-device discovery and transfer remain unverified.

0 commit comments

Comments
 (0)