Skip to content
Back to skills

Android Native

ASecurity

Android native platform APIs beyond UI: Activity/Fragment lifecycle, Android Keystore (hardware-backed key storage), BiometricPrompt, EncryptedSharedPreferences, WorkManager, NFC (NDEF + HCE), Foreground Services, broadcast receivers, ContentProviders, Intents, FileProvider, App Links, ProGuard/R8 rules, permissions model. USE WHEN: user mentions "Android Keystore", "KeyGenParameterSpec", "BiometricPrompt", "EncryptedSharedPreferences", "WorkManager", "NFC", "HCE", "Foreground Service", "Bro...

  • 31 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 8, 2026
ai-agentsgokotlinsqlapidatabasesecuritydocumentation

Works with

  • api
  • mcp

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill android-native --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Android Native?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Android Native
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-android-native/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-android-native)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: android-native
description: |
  Android native platform APIs beyond UI: Activity/Fragment lifecycle,
  Android Keystore (hardware-backed key storage), BiometricPrompt, EncryptedSharedPreferences,
  WorkManager, NFC (NDEF + HCE), Foreground Services, broadcast receivers,
  ContentProviders, Intents, FileProvider, App Links, ProGuard/R8 rules,
  permissions model.

  USE WHEN: user mentions "Android Keystore", "KeyGenParameterSpec", "BiometricPrompt",
  "EncryptedSharedPreferences", "WorkManager", "NFC", "HCE", "Foreground Service",
  "BroadcastReceiver", "ContentProvider", "App Links", "FileProvider", "ProGuard", "R8",
  "Activity lifecycle"

  DO NOT USE FOR: UI with Jetpack Compose - use `mobile/jetpack-compose`
  DO NOT USE FOR: Kotlin language patterns - use `languages/kotlin`
  DO NOT USE FOR: KMP setup - use `mobile/kotlin-multiplatform`
  DO NOT USE FOR: SQLCipher - use `databases/sqlcipher`
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Android Native Platform APIs

> **References**: [keystore-biometric.md](quick-ref/keystore-biometric.md) for Keystore (KeyGenParameterSpec, StrongBox, attestation), BiometricPrompt with crypto object binding, EncryptedSharedPreferences, certificate pinning. [nfc-services.md](quick-ref/nfc-services.md) for NFC (NDEF reading/writing, HCE for payment-style apps), Foreground Services, WorkManager.
>
> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `android`.

## Activity Lifecycle (Modern)

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent { App() }

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.events.collect { handle(it) }
            }
        }
    }
}
```

Key callbacks (rarely overridden in Compose-first apps):
- `onCreate(savedInstanceState)` — initial setup
- `onStart` / `onStop` — visible/invisible
- `onResume` / `onPause` — focused/unfocused
- `onDestroy` — final cleanup
- `onSaveInstanceState(outState)` — survive process death

For Compose: prefer `LifecycleResumeEffect`, `LifecycleEventEffect` over manual lifecycle observers.

## Permissions

`AndroidManifest.xml`:

```xml
<manifest>
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
```

Runtime requests via Activity Result Contracts (see `mobile/jetpack-compose/quick-ref/interop.md`).

## Android Keystore (Hardware-Backed Keys)

The Android Keystore stores cryptographic keys in a container that prevents extraction from the device. On supported hardware (StrongBox-equipped devices), keys live in a tamper-resistant secure element.

### Generate AES key for symmetric encryption

```kotlin
fun generateAesKey(alias: String, requireAuth: Boolean = false): SecretKey {
    val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setKeySize(256)
        .setUserAuthenticationRequired(requireAuth)         // gate by biometric
        .setUserAuthenticationParameters(
            0,                                                // 0 = each use
            KeyProperties.AUTH_BIOMETRIC_STRONG,
        )
        .setIsStrongBoxBacked(true)                          // hardware secure element
        .setRandomizedEncryptionRequired(true)
        .build()
    keyGen.init(spec)
    return keyGen.generateKey()
}
```

### Encrypt / Decrypt with GCM

```kotlin
fun encryptAesGcm(alias: String, plaintext: ByteArray): ByteArray {
    val key = (KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        .getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey

    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)
    val iv = cipher.iv                                  // 12 bytes for GCM
    val ciphertext = cipher.doFinal(plaintext)
    return iv + ciphertext                              // prepend IV
}

fun decryptAesGcm(alias: String, blob: ByteArray): ByteArray {
    val key = (KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        .getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey

    val iv = blob.sliceArray(0..11)
    val ciphertext = blob.sliceArray(12 until blob.size)

    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
    return cipher.doFinal(ciphertext)
}
```

### Generate EC key for signing

```kotlin
fun generateSigningKey(alias: String, requireAuth: Boolean = true): KeyPair {
    val keyGen = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY,
    )
        .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))   // P-256
        .setDigests(KeyProperties.DIGEST_SHA256)
        .setUserAuthenticationRequired(requireAuth)
        .setIsStrongBoxBacked(true)
        .setAttestationChallenge("challenge-bytes".toByteArray())     // for remote attestation
        .build()
    keyGen.initialize(spec)
    return keyGen.generateKeyPair()
}
```

For wallet apps: Keystore P-256 keys are NOT secp256k1 (Bitcoin) — use Keystore for WRAPPING the wallet seed encryption key, store the wrapped seed, and derive secp256k1 keys outside Keystore from the unlocked seed.

See [keystore-biometric.md](quick-ref/keystore-biometric.md) for attestation, key migration, StrongBox detection.

## BiometricPrompt

Modern API (replaces old FingerprintManager). Supports Face/Iris/Fingerprint based on device capability.

```kotlin
class BiometricAuthHelper(private val activity: FragmentActivity) {

    fun authenticate(
        title: String = "Unlock",
        subtitle: String = "Use biometric to access wallet",
        cipher: Cipher? = null,                              // for crypto-bound auth
        onSuccess: (BiometricPrompt.CryptoObject?) -> Unit,
        onError: (Int, CharSequence) -> Unit,
    ) {
        val executor = ContextCompat.getMainExecutor(activity)

        val callback = object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                onSuccess(result.cryptoObject)
            }
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                onError(errorCode, errString)
            }
        }

        val prompt = BiometricPrompt(activity, executor, callback)
        val info = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(subtitle)
            .setNegativeButtonText("Cancel")
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()

        if (cipher != null) {
            prompt.authenticate(info, BiometricPrompt.CryptoObject(cipher))
        } else {
            prompt.authenticate(info)
        }
    }
}
```

### Crypto Object Binding (Recommended)

Bind biometric auth to a Keystore key via `CryptoObject`. The key generated with `setUserAuthenticationRequired(true)` cannot be used until biometric prompt succeeds — stronger than just "ask user, then use key".

```kotlin
fun unlockWalletKey(activity: FragmentActivity, onUnlocked: (Cipher) -> Unit) {
    val keyAlias = "wallet_seed_key"
    val key = (KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        .getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey

    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)               // throws UserNotAuthenticatedException if needed

    BiometricAuthHelper(activity).authenticate(
        cipher = cipher,
        onSuccess = { cryptoObject ->
            cryptoObject?.cipher?.let(onUnlocked)
        },
        onError = { _, _ -> /* ... */ },
    )
}
```

### Check biometric availability

```kotlin
fun canAuthenticate(context: Context): Int =
    BiometricManager.from(context)
        .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)

// Returns:
// BiometricManager.BIOMETRIC_SUCCESS                   = 0
// BIOMETRIC_ERROR_NO_HARDWARE                          = 12
// BIOMETRIC_ERROR_HW_UNAVAILABLE                       = 1
// BIOMETRIC_ERROR_NONE_ENROLLED                        = 11
// BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED             = 15
```

If `BIOMETRIC_ERROR_NONE_ENROLLED`, prompt user to enroll:

```kotlin
val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
    putExtra(
        Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
        BiometricManager.Authenticators.BIOMETRIC_STRONG,
    )
}
context.startActivity(intent)
```

## EncryptedSharedPreferences

Wrapper around SharedPreferences using AES-GCM keys stored in Keystore. Good for storing tokens, small secrets.

```kotlin
implementation("androidx.security:security-crypto:1.1.0-alpha07")

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .setRequestStrongBoxBacked(true)
    .build()

val prefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

prefs.edit { putString("auth_token", token) }
val token = prefs.getString("auth_token", null)
```

For wallet seed storage, **prefer manual Keystore + libsodium-encrypted file** — more control, easier to audit.

## WorkManager (Background Tasks)

For deferrable, guaranteed-execution background work (sync, upload, periodic refresh).

```kotlin
implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("androidx.hilt:hilt-work:1.2.0")
ksp("androidx.hilt:hilt-compiler:1.2.0")
```

```kotlin
@HiltWorker
class SyncWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val repo: WalletRepository,
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            repo.syncAll()
            Result.success()
        } catch (e: Throwable) {
            if (runAttemptCount < 3) Result.retry() else Result.failure()
        }
    }
}

// Schedule
val sync = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.UNMETERED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "wallet_sync",
    ExistingPeriodicWorkPolicy.UPDATE,
    sync,
)
```

Constraint types: NetworkType, RequiresCharging, RequiresBatteryNotLow, RequiresStorageNotLow, RequiresDeviceIdle.

## Foreground Services

For ongoing user-visible work (long sync, audio playback, location tracking). Mandatory for tasks >10 minutes (since Android 14).

```kotlin
class WalletSyncService : Service() {
    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForeground(NOTIFICATION_ID, buildNotification())

        scope.launch {
            try { performSync() } finally { stopSelf() }
        }
        return START_STICKY
    }

    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    private fun buildNotification(): Notification = NotificationCompat.Builder(this, "sync_channel")
        .setContentTitle("Syncing wallet")
        .setSmallIcon(R.drawable.ic_sync)
        .setOngoing(true)
        .build()

    companion object { private const val NOTIFICATION_ID = 1 }
}
```

`AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<service
    android:name=".WalletSyncService"
    android:exported="false"
    android:foregroundServiceType="dataSync" />
```

Android 14 requires explicit `foregroundServiceType` (`dataSync`, `mediaPlayback`, `location`, `connectedDevice`, etc.).

## Notifications

```kotlin
// Channel (one-time setup, e.g., in App.onCreate)
val channel = NotificationChannel(
    "wallet_alerts",
    "Wallet Alerts",
    NotificationManager.IMPORTANCE_HIGH,
).apply {
    description = "Transaction notifications"
}
NotificationManagerCompat.from(context).createNotificationChannel(channel)

// Show
val notification = NotificationCompat.Builder(context, "wallet_alerts")
    .setSmallIcon(R.drawable.ic_btc)
    .setContentTitle("Received 100,000 sats")
    .setContentText("From bc1q...")
    .setStyle(NotificationCompat.BigTextStyle().bigText("..."))
    .setAutoCancel(true)
    .setContentIntent(pendingIntent)
    .build()

NotificationManagerCompat.from(context).notify(notificationId, notification)
```

Android 13+ requires `POST_NOTIFICATIONS` runtime permission.

## App Links / Deep Links

`AndroidManifest.xml`:

```xml
<activity android:name=".MainActivity" android:exported="true">
    <!-- Universal Links (HTTPS, auto-verified) -->
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="bhodl.app" />
    </intent-filter>

    <!-- Custom URI scheme -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="bitcoin" />
        <data android:scheme="lightning" />
    </intent-filter>
</activity>
```

For universal links, host `https://bhodl.app/.well-known/assetlinks.json`:

```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.bhodl.android",
    "sha256_cert_fingerprints": ["AB:CD:..."]
  }
}]
```

## FileProvider

Required to share files (image capture, PDF export) with other apps.

`AndroidManifest.xml`:

```xml
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>
```

`res/xml/provider_paths.xml`:

```xml
<paths>
    <files-path name="exports" path="exports/" />
    <cache-path name="cache" path="/" />
</paths>
```

```kotlin
val file = File(context.filesDir, "exports/labels.bip329")
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)

val intent = Intent(Intent.ACTION_SEND).apply {
    type = "application/json"
    putExtra(Intent.EXTRA_STREAM, uri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(intent, null))
```

## ProGuard / R8

`app/build.gradle.kts`:

```kotlin
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}
```

`proguard-rules.pro` for KMP/Compose/Hilt:

```
# Keep Compose
-keepclasseswithmembers class * {
    @androidx.compose.runtime.Composable <methods>;
}

# Keep kotlinx.serialization
-keep,includedescriptorclasses class com.bhodl.**$$serializer { *; }
-keepclassmembers class com.bhodl.** {
    *** Companion;
}
-keepclasseswithmembers class com.bhodl.** {
    kotlinx.serialization.KSerializer serializer(...);
}

# Keep Hilt-generated
-keep class * extends dagger.hilt.android.internal.managers.ViewComponentManager.FragmentContextWrapper { *; }

# Keep UniFFI generated
-keep class uniffi.** { *; }
-keepclassmembers class uniffi.** { *; }

# Keep JNI
-keepclasseswithmembernames class * {
    native <methods>;
}
```

## Anti-Patterns

| Anti-pattern | Why it's bad | Correct approach |
|---|---|---|
| Storing secrets in SharedPreferences | World-readable on rooted devices | EncryptedSharedPreferences or Keystore-wrapped file |
| Generating wallet keys in Keystore | P-256 only, not secp256k1 | Use Keystore for wrapping, derive Bitcoin keys from unlocked seed |
| `Context` field in static/companion | Memory leak | Use `applicationContext` if global, or weak ref |
| `AsyncTask` (deprecated) | API removed | Use coroutines |
| `BroadcastReceiver` registered without unregister | Leak | Pair `register`/`unregister` in lifecycle |
| `StartActivityForResult` (deprecated) | Brittle | Activity Result Contracts |
| `runOnUiThread { }` from Compose | Wrong abstraction | Use `Dispatchers.Main` or composable side effect |
| Plain HTTP traffic | Insecure | Network Security Config + TLS only |
| Skipping `POST_NOTIFICATIONS` permission (API 33+) | Notifications silently ignored | Request runtime permission |
| Hardcoded `String` for permission | Typo crashes | `Manifest.permission.CAMERA` constant |
| Foreground service without `foregroundServiceType` (API 34+) | Crash | Declare type in manifest |

## Network Security Config

For HTTPS-only + cert pinning:

```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config>
        <domain includeSubdomains="true">api.bhodl.app</domain>
        <pin-set>
            <pin digest="SHA-256">AAAA...</pin>
            <pin digest="SHA-256">BBBB...</pin>     <!-- backup pin -->
        </pin-set>
    </domain-config>
</network-security-config>
```

`AndroidManifest.xml`:

```xml
<application android:networkSecurityConfig="@xml/network_security_config">
```

## When NOT to Use This Skill

| Scenario | Use Instead |
|----------|-------------|
| Compose UI patterns | `mobile/jetpack-compose` |
| Cross-platform Kotlin | `mobile/kotlin-multiplatform` |
| Encrypted database | `databases/sqlcipher` |
| Generic encryption primitives | `security/libsodium` |
| Kotlin language details | `languages/kotlin` |
| Keystore deep dive (StrongBox, attestation, migration) | [keystore-biometric.md](quick-ref/keystore-biometric.md) |
| NFC HCE, WorkManager constraints, Foreground Service types | [nfc-services.md](quick-ref/nfc-services.md) |

Files in this skill

  • SKILL.md18.9 KB
  • quick-ref/keystore-biometric.md13.6 KB
  • quick-ref/nfc-services.md11.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.