Claude
Skills
Sign in
Back

android-native

Included with Lifetime
$97 forever

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`

Design

What this skill does

# 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                        

Related in Design