Documentation
Getting AppGuard into an app is four steps and about ten lines. This page covers setup, the policy model, handling events, and what to check when something does not work.
Requirements
| minSdk | 30 |
|---|---|
| Android Gradle Plugin | 8.0 or newer |
| Gradle | 8.4 or newer, running on JDK 17 or newer |
| Kotlin | 1.9 or newer |
| ABIs | armeabi-v7a, arm64-v8a, x86_64 |
The native detection layer ships prebuilt inside the AAR, so you do not need the NDK or
CMake. If your app restricts ABI splits, make sure the three above are among them — a
missing .so disables native Frida detection.
Add the plugin
Apply the plugin and add the library to your application module:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("pro.paphitis.appguard") version "0.1.0"
}
dependencies {
implementation("pro.paphitis.appguard:android-app-guard:0.1.0")
}
The plugin and the AAR carry the same version, and must stay in step. The plugin writes
check names into BuildConfig that the library parses back, and the library
rejects a name it does not recognise rather than silently skipping the check.
BuildConfig on the APK that ships, so applying it to a library or
JVM module fails the build with an explanatory message.
Add your licence key
A licence key names one base application ID, the environment suffixes it covers, and an expiry. The plugin verifies it during configuration — offline, with no call home — and fails the build if the variant's application ID is not covered. Request one here.
| Tier | For | Term | Price |
|---|---|---|---|
| Development | Evaluation, non-production builds | 3 months | Free |
| Startup | Production, under 10,000 users | 12 months | €100 / year |
| Enterprise | Production, 10,000 users or more | 12 months | €1,000 / year |
Per application ID, VAT included. There is no grace period — the plugin verifies offline, so an expired licence fails the build the moment it lapses. Renew before the expiry date in your key.
The plugin looks in four places, first match wins:
| 1. Build script | appGuard { licenseKey = "AG1.…" } |
|---|---|
| 2. Gradle property | appguard.licenseKey=AG1.… |
| 3. Environment | APPGUARD_LICENSE_KEY=AG1.… |
| 4. File | appguard.license in the module or root project directory |
build.gradle.kts is usually
committed. Put it in ~/.gradle/gradle.properties for developers and in a secret
for CI, and it never reaches version control.
Which environments a licence covers:
Licensed base ID : com.example.app
Suffixes : (production), .qa, .uat, .test
Builds that pass : com.example.app
com.example.app.qa
com.example.app.uat
com.example.app.test
Builds that fail : com.example.app.internal ← not on the licence
The check runs per variant against the resolved application ID, so product flavours and
applicationIdSuffix are both accounted for. Request every environment you ship
to — adding one later means a new key.
Choose a policy per check
Each check is independently ALLOWED, WARNING or
ENFORCED, set per build type. Both blocks are optional; anything you leave out
takes the default in the reference table below.
appGuard {
licenseKey = "AG1.…"
allowedSigningCertificates = listOf("A1:B2:…")
debug {
// Normal on a development machine — running these would mean a
// finding on every single launch.
signature = SecurityPolicy.ALLOWED
installer = SecurityPolicy.ALLOWED
debugger = SecurityPolicy.ALLOWED
emulator = SecurityPolicy.ALLOWED
// Something genuinely wrong with the device, not the build.
root = SecurityPolicy.WARNING
hooking = SecurityPolicy.WARNING
}
release {
signature = SecurityPolicy.ENFORCED
debugger = SecurityPolicy.ENFORCED
emulator = SecurityPolicy.ENFORCED
root = SecurityPolicy.ENFORCED
hooking = SecurityPolicy.ENFORCED
installer = SecurityPolicy.ALLOWED // opt in when you ship store-only
}
}
WARNING while you investigate.
Build types other than debug use the release block. A build type
nobody configured is more likely to ship than to be a development build, and guessing wrong
in that direction is the expensive mistake.
Wire up your Application
Extend SecureApplication and hand it the values the plugin put in
BuildConfig. That is the whole integration — the base class runs the checks on
startup and on a randomised foreground schedule.
class MyApp : SecureApplication() {
override val securityPolicies =
SecurityPolicies.parse(BuildConfig.APPGUARD_POLICIES)
override val allowedSigningCertificates =
BuildConfig.ALLOWED_SIGNING_CERTIFICATES
.split(",")
.filter { it.isNotBlank() }
}
Register it in your manifest:
<application android:name=".MyApp" … >
Not using the plugin? SecurityPolicies has a normal constructor —
SecurityPolicies(signature = SecurityPolicy.ENFORCED, …) — and defaults to
everything enforced except the installer check, so forgetting to configure it fails closed.
Handle security events
Every detection is published to securityEvents, keyed by check. Events are
emitted for WARNING and ENFORCED — an enforced finding is
published before the exception is thrown, so you see the reason rather than only the crash.
lifecycleScope.launch {
(application as SecureApplication).securityEvents
.filter { it.check == SecurityCheck.ROOT }
.collect { event ->
Log.w("AppGuard", "${event.policy}: ${event.message}")
if (event.isFatal) disableSensitiveFeatures()
}
}
Application.onCreate, so you do not have to race the checks to observe them.
isDeviceSecure() gives the aggregate — false once an ENFORCED
check has failed. Warnings do not change it.
Screen protection
Extending SecureActivity is optional and separate from the checks. It applies
FLAG_SECURE (blocking screenshots, screen recording and the recents thumbnail),
filters touches that arrive while another window is overlaid — the mechanism behind
tapjacking — and re-validates integrity at each lifecycle entry point.
class MainActivity : SecureActivity() {
override fun onSecureCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)
}
// Called when validation fails. Terminates the task by default.
override fun onDeviceNotSecure() {
showBlockedScreen()
}
}
Override the defaults per app with a resource:
<!-- res/values/config.xml -->
<resources>
<bool name="security_window_secure_enabled">false</bool>
<bool name="security_touch_filtering_enabled">true</bool>
</resources>
Note onSecureCreate rather than onCreate — the base class needs
to apply window flags before your content view is inflated.
Whitelist your signing certificate
The signature check compares the running APK's signing certificate against
allowedSigningCertificates. Supply SHA-256 fingerprints as colon-hex or base64;
both are accepted.
./gradlew :app:signingReport
ENFORCED in release, that is every user, immediately.
The check requires every signer to be whitelisted, not just one, so an attacker cannot append their certificate to the signing block while keeping yours.
R8 and ProGuard
Nothing to add. The AAR ships its own consumer rules, which keep the JNI boundary intact — the native layer resolves its bridge class by name at load time, so that one class must survive shrinking.
Everything else is deliberately left obfuscatable. Every unobfuscated symbol in a security
library is a signpost telling an attacker which method to hook, so do not add broad
-keep rules for AppGuard classes.
Policy reference
| Check | Detects | debug default | release default |
|---|---|---|---|
signature | Repackaged APK — signing certificate not whitelisted | ALLOWED | ENFORCED |
root | RootBeer signals, Magisk, permissive SELinux | WARNING | ENFORCED |
hooking | Frida, Xposed, runtime instrumentation | WARNING | ENFORCED |
debugger | Java or native debugger, debuggable flag | ALLOWED | ENFORCED |
emulator | Emulator rather than physical hardware | ALLOWED | ENFORCED |
installer | Installed from an untrusted source | ALLOWED | ALLOWED |
installer is off even in release because it produces false positives for
sideloaded enterprise builds and during QA. Turn it on once you ship exclusively through a
store.
Troubleshooting
“variant … is not licensed”
The variant's application ID is not covered. The message lists what the licence does cover;
compare it against the applicationId and any applicationIdSuffix
on that build type. If the suffix is new, you need a reissued licence.
“AppGuard requires a licence key”
Nothing supplied one. The message lists all four sources — the usual cause is a key set in
~/.gradle/gradle.properties on a developer machine but not in CI.
“The AppGuard licence expired on …”
Licences are time-limited and verified offline, so there is no grace period and no way to extend one without a new key. Quote the licence id from the message when you get in touch.
Signature check fails on a store build only
Almost always Play App Signing: the APK users receive is signed by Google's distribution certificate, not your upload certificate. Add both.
Findings on every debug launch
Expected if the debug policy is stricter than the defaults. A debug build is signed with the
debug keystore, is sideloaded, is often on an emulator and frequently has a debugger
attached — all four are findings unless set to ALLOWED.
The app dies at startup in release with no useful stack trace
An ENFORCED finding throws DeviceIntegrityException and then kills
the process shortly afterwards, so an uncaught-exception handler cannot swallow it and keep
a compromised app running. Collect securityEvents to see the reason — it is
published before the throw.