- Requirements
- Building the project
- Deploying via Android MDM
- How the app starts
- Running tests
- Code quality
- Troubleshooting
- JDK 17 or later - Set
JAVA_HOMEenvironment variable - Android SDK - Gradle finds it via:
local.propertiesfile withsdk.dir(auto-created by Android Studio) ✅ Recommended- OR
ANDROID_HOME/ANDROID_SDK_ROOTenvironment variables - Install via Android Studio (easiest)
- Or install command-line tools
- Requires SDK Platform API 33+ and Build Tools 34.0.0+
./gradlew assembleDebugOutput: app/build/outputs/apk/debug/app-debug.apk
./gradlew assembleReleaseOutput: app/build/outputs/apk/release/app-release.apk
Note: By default (without signing configuration), this creates an unsigned APK not suitable for distribution.
Signing configuration is already set up in build.gradle.kts. You just need to provide the keystore and credentials.
One-time setup per developer/machine:
- Create a keystore:
keytool -genkeypair \
-alias fleet-android \
-keyalg RSA \
-keysize 4096 \
-validity 10000 \
-keystore keystore.jks \
-storepass YOUR_PASSWORD \
-dname "CN=Your Name, O=Your Org, L=City, ST=State, C=US"- Create
keystore.propertiesfile in theandroid/directory:
storeFile=/path/to/keystore.jks
storePassword=YOUR_PASSWORD
keyAlias=fleet-android
keyPassword=YOUR_PASSWORD- Build signed release:
# APK (for direct distribution)
./gradlew assembleRelease
# AAB (for Google Play Store)
./gradlew bundleReleaseOutput:
- APK:
app/build/outputs/apk/release/app-release.apk - AAB:
app/build/outputs/bundle/release/app-release.aab
Verify signing:
# APK - use apksigner (in SDK build-tools)
# Find your SDK and build-tools version:
grep sdk.dir local.properties
ls "$(grep sdk.dir local.properties | cut -d= -f2)/build-tools/"
# Then verify:
<sdk-path>/build-tools/<version>/apksigner verify --verbose app/build/outputs/apk/release/app-release.apk
# AAB - use jarsigner (included with JDK)
jarsigner -verify app/build/outputs/bundle/release/app-release.aabThe SHA256 fingerprint is required for MDM deployment. You can get it from your keystore.
keytool -list -v -keystore keystore.jks -alias fleet-android
# Grab SHA256 (remove colons and convert to base64)
echo <SHA256> | tr -d ':' | xxd -r -p | base64Copy the fingerprint for use in the mdm.android_agent.signing_sha256 config option.
For development, each developer publishes a private build of the agent under a unique package name (e.g. com.fleetdm.agent.private.<yourname>) via the Google Play Console and distributes it to the target Fleet Android enterprise by Organization ID.
Who owns the app: Ask a Fleet admin to create the Play Console app com.fleetdm.agent.private.<yourname> and grant you the Admin role on it. That gives you upload + release access without needing your own paid developer account.
-
Configure Fleet with your agent package and signing fingerprint:
Environment variables:
export FLEET_MDM_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.<yourname> export FLEET_MDM_ANDROID_AGENT_SIGNING_SHA256=<SHA256 fingerprint>
Or in your Fleet config file:
mdm: android_agent: package: com.fleetdm.agent.private.<yourname> signing_sha256: <SHA256 fingerprint>
Use the SHA-256 of your upload keystore (see "Getting the SHA256 fingerprint" above for the keytool + base64 conversion).
-
Set
applicationIdinapp/build.gradle.kts:defaultConfig { applicationId = "com.fleetdm.agent.private.<yourname>" // ... } -
Build a signed release (AAB) using the instructions above.
-
Find the Organization ID for your target Fleet Android enterprise:
This is the same as the Android Enterprise ID that Fleet uses for AMAPI calls (format like
LCxxxxxxxx). You can find it in the Fleet UI under MDM Android settings, or in theenterprise_idcolumn of theandroid_enterprisestable in Fleet's DB. -
Upload the AAB in Play Console:
- Open
https://play.google.com/consoleand pick the developer account that ownscom.fleetdm.agent.private.<yourname>(the one the Fleet admin granted you Admin on). - Go to the app → Test and release → pick a track (Internal/Closed/Production) → create a release → upload your AAB → save and review.
- Open
-
Distribute to your Fleet enterprise:
- In the same app, go to Test and release → Advanced settings → Managed Google Play.
- Add the Organization ID you copied in step 4 to the list of organizations allowed to install the app.
- Save.
-
Roll out the release (publish it on the chosen track). Wait ~10 minutes for Play to propagate.
-
Enroll your Android device in Fleet. The agent should appear in your Work profile shortly after. If it's stuck pending, restart the device or check the device's
nonComplianceDetailsviatools/android/android.go -command devices.list ....
If your Play Console app already exists and you only need to grant access to a new Fleet enterprise, repeat steps 4 and 6 for the new Organization ID. No new upload is required.
The Fleet Android agent is designed to run automatically without user interaction. The app starts in three scenarios:
When the app is installed via MDM, Android Device Policy assigns it the COMPANION_APP role. This triggers RoleNotificationReceiverService, which starts the app process and runs AgentApplication.onCreate().
When the device boots, BootReceiver receives the ACTION_BOOT_COMPLETED broadcast and starts the app process, triggering AgentApplication.onCreate().
AgentApplication.onCreate() schedules a ConfigCheckWorker to run every 15 minutes using WorkManager. This ensures the app wakes up periodically even if the process is killed.
Note: WorkManager ensures reliable background execution. The work persists across device reboots and process death.
We don't use ACTION_APPLICATION_RESTRICTIONS_CHANGED to detect MDM config changes because:
- This broadcast can only be registered dynamically (not in the manifest)
- On Android 14+, context-registered broadcasts are queued when the app is in cached state
This means the broadcast won't wake the app immediately when configs change if the app is in the background. WorkManager polling every 15 minutes is the reliable solution for detecting config changes.
./gradlew buildThis runs:
- Compilation (debug + release)
- Unit tests
- Android Lint
- Spotless formatting checks (automatic)
./gradlew testIntegration tests are skipped by default. To run them:
./gradlew test -PrunIntegrationTests=true \
-Pscep.url=https://your-scep-server.com/scep \
-Pscep.challenge=your-challenge-passwordIntegration tests require a real SCEP server. Options:
-
Production-grade SCEP servers:
- Microsoft NDES (Network Device Enrollment Service)
- OpenXPKI
- Ejbca
-
Lightweight test servers:
- micromdm/scep (Docker)
- jscep test server
docker run -p 8080:8080 \
-e SCEP_CHALLENGE=test-challenge-123 \
micromdm/scep:latest./gradlew test -PrunIntegrationTests=true \
-Pscep.url=http://localhost:8080/scep \
-Pscep.challenge=test-challenge-123./gradlew connectedDebugAndroidTestCheck formatting:
./gradlew spotlessCheckAuto-fix formatting issues:
./gradlew spotlessApplyNote: Spotless checks run automatically during ./gradlew build. Run spotlessApply to fix issues before committing.
Run manually:
./gradlew detektNote: Detekt does NOT run automatically in local builds (only in CI). Run manually when needed.
See gradle/libs.versions.toml for complete list.
- Before committing: Run
./gradlew spotlessApplyto fix formatting - Local verification: Run
./gradlew buildto ensure everything passes - Optional: Run
./gradlew detektfor static analysis - Push: CI will run all checks automatically
Clean build:
./gradlew clean buildDelete device from Android MDM:
- Delete Work profile on Android device
- Using
tools/android/android.go, delete the device and delete the associated policy (as of 2025/11/21, Fleet server does not do this)