Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.3.0] - 2026-08-27

### Added

- Android-first manual rebuild with shared Compose Multiplatform Send and Receive UI.
Expand All @@ -14,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Current macOS/Linux DNS-SD/mDNS discovery and registration through JmDNS on eligible IPv4 and IPv6 LAN addresses.
- Separate discovery and registration lifecycle states shared by Android, Desktop, the controller, and UI.
- Stable per-install device identity and advertised dynamic HTTP/file-transfer ports.
- Text offers, receiver Accept/Decline, text transfer, Copy, and Clear.
- One-request direct text delivery with sender name, a 100,000-character limit, Copy, and Clear.
- Android and Desktop multiple-file selection and metadata offers.
- Raw TCP file transfer using one persistent connection per accepted batch.
- Operation-bound file framing with operation ID, index, and size validation plus one final batch result containing receiver success and the completed-file count.
Expand All @@ -27,12 +29,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Application-lifetime network startup and state-driven connection repair.
- Enabled iOS device and Apple-silicon Simulator targets with native Bonjour discovery, document selection, clipboard, Files-visible storage, and streamed TCP transfer implementations.
- Added an iOS-only GitHub Actions workflow for an unsigned Simulator app and optional development-signed iPhone IPA.
- Prepared version `0.2.0` across Android, Desktop, and iOS; retained private Android release signing configuration and the permanent Windows MSI upgrade identity.
- Prepared version `0.3.0` across Android, Desktop, and iOS; retained private Android release signing configuration and the permanent Windows MSI upgrade identity.
- Public architecture, development, roadmap, security, privacy, and contribution documentation.

### Changed

- 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.
- Separated text from file operations: text now delivers directly while idle without an offer, decision, operation ID, waiting state, remote cancellation, or Cancel action.
- Kept file offers, receiver decisions, operation IDs, cancellation, timeouts, progress, and raw TCP streaming unchanged in purpose.
- Replaced the old generated sync implementation with a smaller, manually understood flow.
- Separated Ktor HTTP offer/control messages from raw TCP file bytes.
- Reused one TCP connection for the complete accepted multi-file batch instead of opening one connection per file.
Expand Down
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
### Android → Android

<img src="screenshots/hero-demo.gif" alt="Sync360 Android-to-Android text and file transfer demo" width="1080" />
<sub>Nearby discovery, receiver approval, and direct Android-to-Android text/file transfer.</sub>
<sub>Nearby discovery and direct Android-to-Android text/file transfer.</sub>

### Desktop → Android

Expand All @@ -33,7 +33,7 @@ We have all done it: send a file to ourselves, wait for it to upload, open anoth
Sync360 is for that nearby moment.

```text
open app -> find nearby device -> choose what to send -> receiver approves -> send directly
open app -> find nearby device -> choose text or files -> send directly
```

The current app discovers other Sync360 devices on the same local network and transfers content directly between them. The transfer path does not use an account, cloud storage, or a Sync360 backend. It depends on the local network and the two devices involved.
Expand All @@ -50,8 +50,8 @@ In an initial Windows 11 Ethernet test, the native Windows DNS-SD backend discov

- Discover nearby Android devices with Android NSD/mDNS.
- Advertise dynamic HTTP and file-transfer ports on the local network.
- Send a text offer and let the receiver accept or decline it.
- Transfer accepted text and copy it from the Receive screen.
- Deliver text directly with one HTTP request when the receiver is idle.
- Enforce a 100,000-character text limit and show the sender name with Copy and Clear actions.
- Select images, videos, documents, and multiple files.
- Show file metadata to the receiver before any file bytes are sent.
- Stream file bytes directly over raw TCP without loading an entire file into memory.
Expand Down Expand Up @@ -92,13 +92,14 @@ The current progress UI tracks the exact bytes transferred across the accepted b

Sync360 uses two small networking paths with different jobs:

- **Ktor HTTP is the control plane.** It carries text/file offers, receiver decisions, file metadata, and text payloads.
- **Ktor HTTP handles direct text delivery and the file control plane.** It carries text payloads, file offers, receiver decisions, and file metadata.
- **Raw TCP is the file data plane.** It streams the actual file bytes directly between devices.

```mermaid
flowchart LR
A["Sender device"] -->|"Android NSD or platform Desktop DNS-SD"| B["Receiver device"]
A -->|"Ktor: offer + decision + metadata"| B
A -->|"Ktor: direct text delivery"| B
A -->|"Ktor: file offer + decision"| B
A -->|"Raw TCP: streamed file bytes"| B
B -->|"Platform Downloads writer"| D["Downloads"]
```
Expand All @@ -113,14 +114,12 @@ Android and Desktop start the shared network controller from their application e
SendScreen
-> SendScreenViewModel
-> OutgoingRequestsController
-> POST /sync360/text/offer
-> receiver Accept/Decline
-> accepted receiver waits for the matching text payload
-> POST /sync360/text/transfer
-> ReceiveScreen shows the text
-> POST /sync360/text/deliver with sender name and text
-> receiver atomically accepts only while idle
-> ReceiveScreen shows the sender name and text
```

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.
Text uses one request and has no offer, receiver decision, operation ID, waiting state, remote cancellation, or Cancel action. The UI, outgoing controller, and receiver reject text above 100,000 Kotlin `String.length` units. The receiver checks `Idle` and publishes the complete received text atomically under the incoming-operation mutex; otherwise it reports that it is busy.

### File path

Expand Down Expand Up @@ -237,9 +236,9 @@ macOS/Linux:
2. Connect both devices to the same Wi-Fi network or hotspot.
3. Keep Sync360 open on both devices during the current foreground-only test flow.
4. On the Send screen, wait for the other device to appear.
5. Choose Text or Files, select the nearby device, and send an offer.
6. Accept the offer on the receiving device.
7. Received files will be written to the platform's Downloads folder.
5. For text, enter the content and select the nearby device; idle receivers show it immediately.
6. For files, select the files and nearby device, then accept the offer on the receiver.
7. Accepted files will be written to the platform's Downloads folder.

Some routers enable client isolation and block local device-to-device traffic. If discovery or transfer does not work, try another trusted Wi-Fi network or a phone hotspot.

Expand All @@ -251,7 +250,7 @@ Reload is available only after the current discovery window has stopped while se

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

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.
The current implementation uses cleartext local HTTP and raw TCP. File operation IDs correlate offers, cancellation, and file sockets for correctness, but they are not secret or authenticated. Direct text delivery has no receiver approval. Sync360 does not yet authenticate the sender, encrypt content, or verify file integrity with a cryptographic hash. File receiver approval exists in the UI, but it is not a complete security boundary.

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.

Expand All @@ -278,7 +277,7 @@ Use the current app only for development and testing on private networks you con
Sync360 is not trying to become a chat app, cloud-sync product, or permanent device manager. The product direction stays focused:

```text
find nearby -> approve -> send directly
find nearby -> send text or approve files -> transfer directly
```

## Why the rebuild is intentionally small
Expand Down
4 changes: 2 additions & 2 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ android {
applicationId = "com.liftley.sync360"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 2
versionName = "0.2.0"
versionCode = 3
versionName = "0.3.0"
}

buildFeatures {
Expand Down
9 changes: 4 additions & 5 deletions context.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Sync360 is an Android-first Kotlin Multiplatform / Compose Multiplatform app for nearby sharing over a local network.

```text
open app -> discover nearby device -> choose content -> receiver approves -> send directly
open app -> discover nearby device -> choose text or files -> send directly
```

The old AI-generated sync implementation was removed. The current app is being rebuilt manually so its maintainer can understand and own the complete discovery, request, transfer, and storage path.
Expand All @@ -16,7 +16,7 @@ The old AI-generated sync implementation was removed. The current app is being r
- Windows discovery/registration through the operating system `dnsapi.dll` DNS-SD API on all interfaces.
- Current macOS/Linux discovery/registration through JmDNS on eligible IPv4 and IPv6 LAN addresses.
- Application-lifetime network startup with separate discovery and registration lifecycle states.
- Ktor HTTP offers, receiver decisions, metadata, and text payloads.
- Ktor HTTP direct text delivery plus file offers, receiver decisions, and metadata.
- Raw TCP streaming for file bytes.
- Multiple files sent sequentially over one accepted-batch connection.
- Android file access through `ContentResolver` and Downloads writing through `MediaStore`.
Expand Down Expand Up @@ -45,8 +45,7 @@ Compose screen -> ViewModel -> controller/service -> common contract -> platform
Ktor HTTP is the control plane:

```text
POST /sync360/text/offer
POST /sync360/text/transfer
POST /sync360/text/deliver
POST /sync360/file/offer
POST /sync360/operation/cancel
```
Expand Down Expand Up @@ -78,7 +77,7 @@ Current shared transfer constants use a 512 KiB payload buffer, 5-second connect

## Important limitations

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.
Sync360 currently uses cleartext local HTTP and raw TCP. Direct text has no receiver approval or operation ID. File operation IDs correlate protocol messages and sockets but do not authenticate a peer. File offers require receiver approval, but the app has 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.

For detailed and current information, read:

Expand Down
2 changes: 1 addition & 1 deletion desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ compose.desktop {
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Sync360"
packageVersion = "0.2.0"
packageVersion = "0.3.0"
appResourcesRootDir.set(
project.layout.projectDirectory.dir("packaging/app-resources")
)
Expand Down
18 changes: 9 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ app starts
-> FileTransferReceiver opens an OS-assigned TCP port
-> NetworkServices advertises both ports through DNS-SD/mDNS
-> nearby Sync360 devices are resolved into NearbyDevice
-> sender posts a text or file offer through Ktor HTTP
-> receiver accepts or declines
-> accepted text continues through HTTP
-> sender delivers text directly or posts a file offer through Ktor HTTP
-> idle receiver publishes the text, or the receiver accepts/declines the file offer
-> accepted file bytes stream through one raw TCP connection
-> platform DownloadsWriter saves the files
```
Expand Down Expand Up @@ -74,8 +73,8 @@ ViewModels launch UI-facing work. They do not implement platform APIs or socket
### Controllers

- `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.
- `OutgoingRequestsController` creates offers, calls the Ktor client, and starts accepted file transfers.
- `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.
- `OutgoingRequestsController` validates and delivers text, creates file offers, and starts accepted file transfers.
- `IncomingServerRequestsController` uses one operation mutex to atomically admit direct text only while idle and to serialize file Accept/Decline/Cancel races. Text follows `Idle -> TextReceived -> Idle`; files follow `Idle -> IncomingFileOffer -> WaitingForFiles -> ReceivingFiles -> FilesReceived`. File states retain their request, so sender identity, operation ID, and acceptance phase remain derived from state.

### Discovery

Expand All @@ -99,16 +98,17 @@ The macOS/Linux JmDNS fallback starts on eligible IPv4 and IPv6 addresses from e

## Control plane: Ktor HTTP

Ktor carries offers, decisions, metadata, and text:
Ktor carries direct text plus file offers, decisions, and metadata:

```text
POST /sync360/text/offer
POST /sync360/text/transfer
POST /sync360/text/deliver
POST /sync360/file/offer
POST /sync360/operation/cancel
```

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.
Text is delivered in one request containing the sender device name and text. It has no offer, decision, operation ID, waiting state, or cancellation route. Text above 100,000 Kotlin `String.length` units is rejected, and the receiver atomically checks `Idle` and publishes `TextReceived` under the operation mutex.

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

## File data plane: raw TCP

Expand Down
8 changes: 4 additions & 4 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Windows:

## Preparing public packages

The current package version is `0.2.0`.
The current package version is `0.3.0`.

Android release APKs must use the maintainer's permanent private signing key. Copy `keystore.properties.example` to the ignored `keystore.properties` file and set:

Expand Down Expand Up @@ -88,8 +88,8 @@ The Windows `upgradeUuid` must remain unchanged for the lifetime of Sync360, and
1. Connect both devices to the same trusted Wi-Fi network or hotspot.
2. Open Sync360 on both devices and keep it in the foreground during current testing.
3. Wait for the other device to appear on the Send screen.
4. Test a text offer: Accept, Decline, transfer, Copy, and Clear.
5. Test one file, multiple files, and cancellation.
4. Test direct text delivery while idle and busy, the 100,000/100,001 boundaries, sender name, Copy, and Clear.
5. Test file Accept/Decline, one file, multiple files, and cancellation.
6. Confirm completed files appear in Downloads.
7. Resize the Desktop window and verify compact single-pane navigation and the wider 50/50 Send/Receive layout.

Expand Down Expand Up @@ -121,7 +121,7 @@ Useful source locations:
- `WindowsNetworkServices` — Windows DNS-SD registration, discovery, resolution, cancellation, and shared-state mapping.
- `WindowsDnsSdApi` — focused JDK Foreign Function and Memory bindings for `dnsapi.dll`.
- `JvmNetworkServices` — current macOS/Linux JmDNS registration, discovery, repair cleanup, and IPv4/IPv6 LAN-interface selection.
- `Sync360HttpServer` / `Sync360HttpClient` — offer and text routes.
- `Sync360HttpServer` / `Sync360HttpClient` — direct text delivery and file control routes.
- `OutgoingRequestsController` / `IncomingServerRequestsController` — send/receive coordination.
- platform `FileTransferSender`, `FileTransferReceiver`, and `DownloadsWriter` implementations — file bytes and storage.

Expand Down
Loading
Loading