Development

How to build, run, and test every part — and how to drive the whole loop end to end.

Prerequisites

  • JDK — the Foojay toolchain resolver (settings.gradle.kts) downloads a matching JDK automatically, so no manual JAVA_HOME is required. Modules compile to JVM 11 bytecode.
  • Android SDK — for the agent. Point local.properties at it (sdk.dir=…); this file is git-ignored.
  • An emulator — to run the agent as Device Owner: a Google APIs image with no account (covers both Device Owner and FCM — see firebase-setup.md).
  • Everything is driven by the Gradle wrapper (./gradlew); the configuration cache and build cache are enabled in gradle.properties.

Version catalog

All versions live in gradle/libs.versions.toml and are referenced via libs.* / libs.plugins.*.

Component Version
Kotlin 2.4.0
Android Gradle Plugin (AGP) 9.0.1
Compose Multiplatform 1.11.1
Ktor 3.5.0
kotlinx-serialization 1.11.0
kotlinx-coroutines 1.11.0
Firebase Admin SDK (server) 9.9.0
Firebase BoM (agent) 34.14.0
firebase-messaging (agent) pinned by the BoM
WorkManager (androidx.work) 2.10.5
google-services plugin 4.4.4
Foojay toolchain resolver 1.0.0
JVM target 11
Android compile / target / min SDK 36 / 36 / 24

A few conventions worth knowing (more in ../CLAUDE.md):

  • Ktor server artifacts use the -jvm suffix; client artifacts use bare module ids (Gradle metadata picks the variant).
  • The kotlin.plugin.serialization plugin is applied only in :protocol (where @Serializable is declared). Consumers just need the types on the classpath.
  • Project dependencies use type-safe accessors (projects.protocol, …).

Build & run

Target Command
Agent — build APK ./gradlew :agent:assembleDebug
Agent — install to device/emulator ./gradlew :agent:installDebug
Server (Netty, port 8081) ./gradlew :server:run
Desktop console ./gradlew :app:desktopApp:run
Desktop console — hot reload ./gradlew :app:desktopApp:hotRun --auto
Web console (Wasm) ./gradlew :app:webApp:wasmJsBrowserDevelopmentRun
Web console (JS) ./gradlew :app:webApp:jsBrowserDevelopmentRun
iOS open ../app/iosApp in Xcode

The built agent APK lands at agent/build/outputs/apk/debug/agent-debug.apk.

The Gradle project lock

:server:run and the console dev-runs are long-lived tasks that hold the Gradle project lock — any other ./gradlew command blocks until they stop. So for end-to-end work, build artifacts first, then start the server, then drive everything else with adb/curl (not Gradle). Or run the server from a second checkout/terminal that doesn’t contend for the lock.

Tests

There’s no single all-targets task; run per target.

Scope Command
Protocol ./gradlew :protocol:jvmTest
Server ./gradlew :server:test (e.g. --tests "com.arganaemre.mdmcore.StatusTest")
Shared — Android host ./gradlew :app:shared:testAndroidHostTest
Shared — JVM ./gradlew :app:shared:jvmTest
Shared — Wasm ./gradlew :app:shared:wasmJsTest
Shared — JS ./gradlew :app:shared:jsTest
Shared — iOS sim ./gradlew :app:shared:iosSimulatorArm64Test

What the suites cover:

  • :protocolComplianceRuleTest (the isSatisfiedBy rule) and ProtocolSerializationTest (wire round-trips; missing optional fields fall back to defaults). These run on every common target.
  • :server (Ktor testApplication) — ApplicationTest (health), WireTest (enroll → pull → report), PolicyEditTest (PUT bumps the version and flips a device to STALE), StatusTest (all four ComplianceState values; the server recomputes and ignores the agent’s compliant flag), TokenTest (202 on enrolled, 404 on unknown).

DeviceStore is a shared in-memory singleton, so each server test enrolls and uses its own returned DeviceId rather than assuming device-1.

End-to-end recipe

Run one full enroll → enforce → report → edit → drift → re-comply cycle.

# 1) Build the agent APK FIRST, before the server grabs the project lock.
./gradlew :agent:assembleDebug

# 2) Start a fresh Google APIs emulator with no account signed in.

# 3) Start the server (holds the lock — leave it running in this terminal).
./gradlew :server:run         # http://localhost:8081

# 4) In ANOTHER terminal: install the agent and make it Device Owner.
adb install -r agent/build/outputs/apk/debug/agent-debug.apk
adb shell dpm set-device-owner com.arganaemre.mdmcore/.MdmDeviceAdminReceiver

# 5) Launch the agent. It enrolls (device-1), enforces the default policy
#    (cameraDisabled=true), and reports. Confirm from the host:
curl -s localhost:8081/devices            # device-1 -> COMPLIANT

# 6) Change the desired policy (via curl, or the desktop console's camera toggle).
curl -s -X PUT localhost:8081/devices/device-1/policy \
  -H 'Content-Type: application/json' -d '{"cameraDisabled":false}'
#    Version bumps; if FCM is configured the doorbell wakes the agent now.

# 7) Watch device-1 go STALE -> COMPLIANT as the agent re-syncs.
curl -s localhost:8081/devices/device-1/status

Prefer the GUI for step 6? Run ./gradlew :app:desktopApp:run (from a checkout that isn’t holding the lock) and flip the camera switch on the device card — see console.md.

If something doesn’t behave, see troubleshooting.md.