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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,12 @@ jobs:
working-directory: bindings/java
run: gradle --no-daemon --stacktrace build publishAllPublicationsToStagingRepository

# The example app against the same library, so it cannot rot: two
# engines pair, encrypt and exchange messages in one JVM.
- name: Run the Java example
working-directory: examples/java-app
run: gradle --no-daemon --stacktrace run

security:
name: Dependency Audit
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion bindings/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ProtocolConfig config = new ProtocolConfig(
"com.example.app", "default",
false, false, true, false, false, false, // ble, wifi direct, internet, reticulum, nostr, prefer online
(byte) 8, true, true, true, true, // ttl, encryption, auto key exchange, store pending, require encryption
64L, 4096L, 60_000L, OverflowPolicy.DROP_OLDEST,
64L, 4096L, 86_400_000L, OverflowPolicy.DROP_OLDEST, // pending queue: per peer, global, ttl (24 h), overflow
false, 256, true, true, false, false,
true, true, true, false, true, true, true,
null, null,
Expand Down Expand Up @@ -114,6 +114,8 @@ library for your platform from the release's assets, and give the JVM its path:
path. Run on JDK 22 or later with `--enable-native-access=ALL-UNNAMED`. On
Android use the AAR (`com.offlineprotocol:offline-protocol-sdk`) instead.

A complete, runnable program is in [examples/java-app](../../examples/java-app/README.md).

## Telemetry

Pass `TelemetryConfig.os` and `osMajor` as the platform you actually run on:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ static ProtocolConfig config() {
"com.example.java", "java-consumer",
false, false, true, false, false, false, // ble, wifi direct, internet, reticulum, nostr, prefer online
(byte) 8, true, true, true, true, // initial ttl, encryption, auto key exchange, store pending, require encryption
64L, 4096L, 60_000L, OverflowPolicy.DROP_OLDEST,
64L, 4096L, 86_400_000L, OverflowPolicy.DROP_OLDEST, // pending queue: per peer, global, ttl (24 h), overflow
false, 256, true, true, false, false, // edge-driven DM, group size, group relay x2, admin commits, transport identity
true, true, true, false, true, true, true, // binary wire, nostr x3, compact envelope, rich payload, recovery
null, null, // mesh relay, custody
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ in here fails **silently** when violated.
| [Offline Demo](../examples/demo-app/README.md) | The reference React Native app: discovery, 1:1 chat, groups, mesh services and telemetry in one place |
| [Service Discovery](../examples/service-discovery-app/README.md) | React Native app registering, discovering and invoking services over the mesh |
| [Nostr Transport](../examples/nostr-example/README.md) | Minimal React Native app using the Nostr relay transport |
| [Java](../examples/java-app/README.md) | Two devices in one JVM pairing, messaging, sending a file and forming a group over the Java binding |
| `cargo run -p offline-protocol --example replicated_notes` | Replicated documents: open a store, edit, persist, reopen |
| `cargo run -p offline-protocol-data --example offline_merge` | What two people editing the same document offline actually get back |

Expand Down
84 changes: 84 additions & 0 deletions examples/java-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Java example

Two devices, Alice and Bob, in one JVM: they pair, establish an end-to-end
encrypted session, use conversation signals, transfer a file, and create an
MLS group, using the
[Java binding](../../bindings/java/README.md).

```
alice off1q89jrev...
bob off1q9jxmhy...
ok: bob and alice are paired
ok: bob sees alice online
ok: bob sees alice typing
ok: bob received "hello bob" (encrypted end to end)
ok: alice received a read receipt
ok: alice received a threaded reply
ok: bob received the original file bytes
ok: bob joined the MLS group
ok: bob received the group message
alice transport [Internet]
group members [off1..., off1...]
done
```

## What it shows

- Creating an engine from a `ProtocolConfig` (every field spelled out: the
generated Java record has no defaults, see
[J2](../../docs/bridges/java.md#j2-the-generated-records-have-no-defaults)).
- The engine's own encrypted file stores (`initializeMlsWithFileStores`), so no
storage provider is needed.
- Pairing from events: the app reads `pollEvent()`, and answers
`connection_request_received` and `connection_accepted` with the key package
exchange.
- Presence and typing indicators, followed by a normal encrypted message, a
read receipt tied to its message ID, and a high-priority rich reply threaded
to the original message.
- `sendFile` with byte-for-byte verification of the reassembled `file_received`
event. That event contains base64-encoded data for the host to save or use.
- Creating an MLS group, inviting the paired peer, and sending a group message.
- Reading active transports and group membership back from the SDK.
- A transport's job, in a dozen lines: take what an engine wants to send
(`internetGetNextMessage`) and hand it to the engine it is addressed to
(`internetMessageReceived`), then acknowledge the successful carrier write
(`internetConfirmSent`). A real app does this over a radio or a socket.
Everything else here, the pairing, the MLS session and the encryption, is the
engine's own.

The in-process carrier delivers frames directly between the two engines. It
does not act as an internet relay or record transport metrics; the event checks
show that the SDK processed each feature end to end.

## Run it

You need JDK 22 or later and the native library.

```bash
cargo build -p offline-protocol-uniffi # from the repository root
cd examples/java-app
gradle run # Gradle 9; -PnativeLibrary=<file> for another library
```

`settings.gradle.kts` includes `bindings/java` as a composite build and
substitutes it for the published coordinate, so the example runs against the
source in this checkout. In your own project, drop that block and depend on a
release from Maven Central:

```kotlin
dependencies {
implementation("com.offlineprotocol:offline-protocol-java:<version>")
}
```

Run on JDK 22 or later with `--enable-native-access=ALL-UNNAMED` and the
library's path in `-Duniffi.component.offline_protocol.libraryOverride=<file>`
(or on `java.library.path`); `build.gradle.kts` does both for `gradle run`.

## Where to go next

- Callbacks (`EventCallback`, transports, storage providers) run on the
engine's thread and must not call back in:
[J4](../../docs/bridges/java.md#j4-callbacks-run-on-the-engines-thread-and-must-not-call-back-in).
- The host fills the telemetry platform:
[J3](../../docs/bridges/java.md#j3-the-host-fills-the-telemetry-platform).
35 changes: 35 additions & 0 deletions examples/java-app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
plugins {
application
}

// Run Gradle on JDK 22 or later: the foreign function API the binding uses is
// final in Java 22.
tasks.withType<JavaCompile>().configureEach {
options.release.set(22)
}

dependencies {
implementation("com.offlineprotocol:offline-protocol-java:0.0.0")
// The engine reports events as JSON; any JSON library reads them.
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
}

application {
mainClass.set("com.example.offlineprotocol.Main")
// The binding calls native code, and the JVM asks permission for that.
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
}

// The native library the binding loads. Build it first (cargo build -p
// offline-protocol-uniffi); -PnativeLibrary names another file.
tasks.named<JavaExec>("run") {
val os = System.getProperty("os.name").lowercase()
val fileName = when {
os.contains("win") -> "offline_protocol_uniffi.dll"
os.contains("mac") -> "liboffline_protocol_uniffi.dylib"
else -> "liboffline_protocol_uniffi.so"
}
val library = providers.gradleProperty("nativeLibrary")
.orElse(layout.projectDirectory.file("../../target/debug/$fileName").asFile.absolutePath)
systemProperty("uniffi.component.offline_protocol.libraryOverride", library.get())
}
23 changes: 23 additions & 0 deletions examples/java-app/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// The example depends on the Java binding by its published coordinate. The
// composite build below substitutes the checkout's copy for it, so the example
// runs against the source in this repository. In your own project, drop this
// block and depend on the version you want from Maven Central.
pluginManagement {
repositories {
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositories {
mavenCentral()
}
}

rootProject.name = "offline-protocol-java-example"

includeBuild("../../bindings/java") {
dependencySubstitution {
substitute(module("com.offlineprotocol:offline-protocol-java")).using(project(":"))
}
}
Loading
Loading