<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #36890
7.8 KiB
Fleet Android agent
- Requirements
- Building the project
- Deploying via Android MDM
- How the app starts
- Running tests
- Code quality
- Troubleshooting
Requirements
- 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+
Building the project
Debug build
./gradlew assembleDebug
Output: app/build/outputs/apk/debug/app-debug.apk
Release build
./gradlew assembleRelease
Output: app/build/outputs/apk/release/app-release.apk
Note: By default (without signing configuration), this creates an unsigned APK not suitable for distribution.
Signing release builds
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 bundleRelease
Output:
- 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.aab
Getting the SHA256 fingerprint
The 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 | base64
Copy the fingerprint for use in FLEET_DEV_ANDROID_AGENT_SIGNING_SHA256
Deploying via Android MDM (development)
This feature is behind the feature flag FLEET_DEV_ANDROID_AGENT_PACKAGE. Requires FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS to be set in your workarea to get the Google Play URL.
- Set these env vars on your Fleet server:
export FLEET_DEV_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.<yourname>
export FLEET_DEV_ANDROID_AGENT_SIGNING_SHA256=<SHA256 fingerprint>
- Change the
applicationIdinapp/build.gradle.kts:
defaultConfig {
applicationId = "com.fleetdm.agent.private.<yourname>"
// ...
}
-
Build a signed release (AAB) using the instructions above.
-
Get the Google Play URL:
# Run from top-level directory of the working tree
go run tools/android/android.go --command enterprises.webTokens.create --enterprise_id '<your-enterprise-id>'
-
Upload your signed app in the Private apps tab using the URL from the previous step.
-
Wait ~10 minutes for Google Play to process the upload.
-
Enroll your Android device.
The agent should start installing shortly. Check Google Play in your Work profile. If it shows as pending, try restarting the device.
How the app starts
The Fleet Android agent is designed to run automatically without user interaction. The app starts in three scenarios:
1. On installation (COMPANION_APP role)
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().
2. On device boot
When the device boots, BootReceiver receives the ACTION_BOOT_COMPLETED broadcast and starts the app process, triggering AgentApplication.onCreate().
3. Periodically every 15 minutes
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.
Why not ACTION_APPLICATION_RESTRICTIONS_CHANGED?
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.
Full build with tests
./gradlew build
This runs:
- Compilation (debug + release)
- Unit tests
- Android Lint
- Spotless formatting checks (automatic)
Running tests
Unit tests (JVM)
./gradlew test
Integration tests (with real SCEP server)
Integration tests are skipped by default. To run them:
./gradlew test -PrunIntegrationTests=true \
-Pscep.url=https://your-scep-server.com/scep \
-Pscep.challenge=your-challenge-password
Setting Up a Test SCEP Server
Integration 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 SCEP Server (Easiest)
docker run -p 8080:8080 \
-e SCEP_CHALLENGE=test-challenge-123 \
micromdm/scep:latest
Running Integration Tests
./gradlew test -PrunIntegrationTests=true \
-Pscep.url=http://localhost:8080/scep \
-Pscep.challenge=test-challenge-123
Instrumented tests (requires emulator/device)
./gradlew connectedDebugAndroidTest
Code quality
Formatting with Spotless (ktlint)
Check formatting:
./gradlew spotlessCheck
Auto-fix formatting issues:
./gradlew spotlessApply
Note: Spotless checks run automatically during ./gradlew build. Run spotlessApply to fix issues before committing.
Static analysis with Detekt
Run manually:
./gradlew detekt
Note: Detekt does NOT run automatically in local builds (only in CI). Run manually when needed.
Dependencies
See gradle/libs.versions.toml for complete list.
Development workflow
- 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
Troubleshooting
Clean build:
./gradlew clean build
Delete 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)