Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
| [`compose-preview/`](compose-preview/) | Renders Jetpack Compose `@Preview` functions on-device — no full app build or run. |
| [`ai-literacy-course/`](ai-literacy-course/) | Bundles Learn AI Anywhere's offline "Introduction to AI" course (26 videos + interactive activities) and plays it full-screen, fully offline. |
| [`layout-editor/`](layout-editor/) | Visual drag-and-drop editor for Android XML layouts. |
| [`pair/`](pair/) | Real-time pair programming across two devices on the same WiFi — host or join a session and sync edits and cursor presence live over the local network. |

## Building a plugin

Expand Down
4 changes: 2 additions & 2 deletions ndk-installer-plugin/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:label="NDK Installer"
android:label="NDK Installer (64-bit only)"
android:theme="@android:style/Theme.Material.Light.NoActionBar">

<meta-data
Expand All @@ -11,7 +11,7 @@

<meta-data
android:name="plugin.name"
android:value="NDK Installer" />
android:value="NDK Installer (64-bit only)" />

<meta-data
android:name="plugin.version"
Expand Down
9 changes: 9 additions & 0 deletions pair/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.gradle/
build/
.idea/
local.properties
*.iml
.DS_Store
captures/
.externalNativeBuild/
.cxx/
55 changes: 55 additions & 0 deletions pair/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Pair

A plugin for [CodeOnTheGo](https://github.com/appdevforall/CodeOnTheGo) that turns the IDE into a real-time collaborative editor. Two phones on the same WiFi share one editing session: one device **hosts**, others **join** by typing the host's `ip:port` or scanning its QR code, and from then on edits, cursors, and file opens flow between devices as they happen — no server, no cloud, no signup.

It surfaces as a **Pair** tab in the editor. Host a session and an invite card shows the address and a QR code; join one and the peer list fills in. When the host types in `MainActivity.kt`, the guest sees the same file open and the text arrive keystroke by keystroke, with each peer's live position shown in the peer list.

## Building

```sh
cd pair
./gradlew clean assemblePlugin
```

The `.cgp` lands in `build/plugin/`. Install it from inside CodeOnTheGo via the Plugin Manager. Always `clean` first — the plugin builder copies the built APK into the `.cgp` and then deletes the source APK, so an incremental build can package an empty artifact.

## How it works

- **Pair tab** — contributes an editor tab and sidebar entry via `EditorTabExtension` + `UIExtension`; the whole UI is Jetpack Compose (Home, Host, Guest, and QR-scan screens).
- **Edit sync** — subscribes to the host IDE's `DocumentChangeEvent` on the EventBus for local edits and replays remote edits through `IdeEditorService.replaceRange(...)`. A single loopback guard stops an applied remote edit from rebroadcasting as a local one.
- **File-relative identity** — a file's wire identity is its path **relative to the project root** (`IdeProjectService.getCurrentProject().rootDir`). The sender strips the root; the receiver re-anchors to its own, so a session works across two devices whose projects live at different absolute paths.
- **Presence** — cursor positions ride alongside edits; each peer gets a color, shown in the peer list and (with the extended API below) as an inline caret inside the editor.
- **Transport** — a star topology over `ws://`: the host runs a `WebSocketServer`, each guest opens one `WebSocketClient`, and the host echoes messages to the other guests. Messages are compact JSON (`hi` / `edit` / `cur` / `fo` / `fc` / `ff` / `sync` / `bye`).
- **Conflict handling** — per-file sequence numbers with the host as authority; a divergence flags the session *out of sync* rather than dropping the edit, and the host can push an authoritative resync of the full file.
- **Session history** — past sessions persist as JSON under `IdeEnvironmentService.getPluginDataDirectory()`; the Home screen lists them to rename, delete, or reconnect.

## Source layout

```
pair/
├── build.gradle.kts, settings.gradle.kts, proguard-rules.pro
└── src/main/
├── AndroidManifest.xml plugin id, main class, icons, permissions
├── assets/ icon_day.png, icon_night.png (190×190 interlocking-rings mark)
└── kotlin/com/appdevforall/pair/plugin/
├── PairPlugin.kt IPlugin + EditorTabExtension + UIExtension entry
├── data/ wire protocol, WebSocket server/client, session store
├── domain/ EditBroker orchestrator, observer/applier, path mapping, peer registry
├── ui/ Compose theme, components, and screens
└── util/ LAN discovery, QR decode
```

## Dependencies from `libs/`

- `../libs/plugin-api.jar` — the plugin API surface (`IPlugin`, extensions, `Ide*Service`); `compileOnly`, provided by the IDE at runtime.
- `../libs/gradle-plugin.jar` — the `com.itsaky.androidide.plugins.build` Gradle plugin that packages the `.cgp`.
- `../libs/eventbus-events.jar` — the editor and file `*Event` types Pair subscribes to on the EventBus.
- `../libs/shared.jar` — `com.itsaky.androidide.models.Range`, the type of `DocumentChangeEvent.changeRange` that Pair reads.

Jetpack Compose is linked `compileOnly` (host-provided), not bundled.

> **Note:** Pair requires an extended `plugin-api` beyond the current `stage` baseline — `IdeProjectService.openProject(File)` (open a project after a pull-model sync) and `IdeEditorService.showPeerCursor` / `hidePeerCursor` / `clearPeerCursors` (inline remote-cursor decoration). These land on the `feat/ADFA-4419-remote-peer-editor-decoration` branch. If `assemblePlugin` fails with unresolved references to those symbols, the shared `libs/` jars are older than the API Pair needs; refresh them from a CodeOnTheGo build that includes the extensions (`../scripts/update-libs.sh --local <path-to-CodeOnTheGo> --ref feat/ADFA-4419-remote-peer-editor-decoration`).

## License

Pair is an open-source example plugin for Code on the Go. Its source is licensed per the surrounding `plugin-examples` repository (see `LICENSE` at the repo root). It makes no cloud calls — all traffic stays on the local network between the paired devices.
105 changes: 105 additions & 0 deletions pair/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.itsaky.androidide.plugins.build")
}

pluginBuilder {
pluginName = "pair"
}

android {
namespace = "com.appdevforall.pair.plugin"
compileSdk = 34

defaultConfig {
applicationId = "com.appdevforall.pair.plugin"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0.0"

ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}

buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("debug")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

buildFeatures {
compose = true
}

packaging {
resources {
excludes += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/INDEX.LIST",
"META-INF/io.netty.versions.properties"
)
}
}
}

dependencies {
compileOnly(files("../libs/plugin-api.jar"))
compileOnly(files("../libs/eventbus-events.jar"))
compileOnly(files("../libs/shared.jar"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker (build fails). ../libs/shared.jar doesn't exist in the repo's libs/ (only common.jar, eventbus-events.jar, gradle-plugin.jar, idetooltips.jar, plugin-api.jar). assemblePlugin fails here:

> Failed to transform shared.jar to match attributes ...
   > File/directory does not exist: .../libs/shared.jar

The jar providing com.itsaky.androidide.models.Range needs to be committed to libs/, or this dependency reworked to use what's already there.


implementation("com.google.android.material:material:1.10.0")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

implementation("org.java-websocket:Java-WebSocket:1.5.6")
implementation("org.slf4j:slf4j-nop:1.7.36")

compileOnly("org.greenrobot:eventbus:3.3.1")

val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
compileOnly(composeBom)
compileOnly("androidx.compose.ui:ui")
compileOnly("androidx.compose.ui:ui-graphics")
compileOnly("androidx.compose.ui:ui-tooling-preview")
compileOnly("androidx.compose.foundation:foundation")
compileOnly("androidx.compose.material3:material3")
compileOnly("androidx.compose.runtime:runtime")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")

debugImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui")
debugImplementation("androidx.compose.ui:ui-graphics")
debugImplementation("androidx.compose.foundation:foundation")
debugImplementation("androidx.compose.material3:material3")
debugImplementation("androidx.compose.runtime:runtime")
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
6 changes: 6 additions & 0 deletions pair/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
android.useAndroidX=true
android.nonTransitiveRClass=true
kotlin.code.style=official
Binary file added pair/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions pair/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading