# Android Expression 2

URL: https://docs.bithuman.ai/examples/android-expression-2

> A complete Kotlin app that renders a talking Expression 2 avatar on an Android phone: clone it, add your API secret, build and run.

*Capture: The wise-pup avatar speaking in the expression2-hello app on a Galaxy S25+. Captured on Samsung Galaxy S25+ (Android) · expression2-android 0.4.9 · wise-pup (Expression 2) · 2026-09-23.* (https://docs.bithuman.ai/examples/android-expression-2/clip.mp4)

The app downloads the `wise-pup` sample avatar once, renders every frame of a speech clip on the phone, then plays the audio with the frames in sync. Tap the screen to replay.

## Requirements

| You need | Notes |
|---|---|
| A physical `arm64-v8a` Android phone, USB debugging on | emulators cannot load the engine |
| JDK 17 and an Android SDK with platform 35 | the project pins Gradle 8.11.1 and Android Gradle Plugin 8.7.3 |
| `adb` on your `PATH` | it ships in `$ANDROID_HOME/platform-tools` |
| An [API secret](https://docs.bithuman.ai/start/api-secret) | the engine bills session time, talking or idle |

## Get the code

```bash
git clone https://github.com/bithuman-product/bithuman-examples.git
cd bithuman-examples/android/expression2-hello
```

## Set your API secret

Write `local.properties` next to `settings.gradle.kts`. The file is git-ignored.

```properties
sdk.dir=/path/to/your/Android/sdk
bithuman.apiSecret=<your API secret>
```

`BITHUMAN_API_SECRET` in the environment works instead of the second line.

## Run it

```bash
./gradlew :app:assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n com.example.x2hello/.MainActivity
adb push ../../python/quickstart/speech.wav /storage/emulated/0/Android/data/com.example.x2hello/files/speech.wav && adb shell am start -S -n com.example.x2hello/.MainActivity
```

The first launch creates the app's files folder and says that `speech.wav` is missing; the push fills it and the restart renders. Any 16 kHz mono 16-bit WAV works.

## Expected output

`adb logcat -s X2HELLO` shows the download, the engine starting on the phone's NPU, then:

```text
rendered 277 frames in … s — playing…
277 frames, 13.87 s — tap to replay
```

277 frames for 13.87 seconds of audio is 20 frames a second. On the first launch after install, most of the wait is the one-time download (about 160 MB) and `create()` preparing the model for the accelerator; later launches reuse it, and the frames themselves render in a few seconds.

## How it works

`MainActivity.kt` does four things, all from `ai.bithuman:expression2-android`:

1. sets your API secret once with `Expression2Credential.set(BuildConfig.BITHUMAN_API_SECRET)`, which covers the download and the session;
2. downloads the avatar with `Expression2ModelStore(this).fetch(agentCode)` (`A23WJF0199` by default);
3. opens it with `Expression2Avatar.create(this, model, options)` on a background thread;
4. calls `feed(pcm)` and `flushTail()`, then `pull(frame)` until every frame is out: one frame per 50 ms of audio.

The calls, the live-streaming loop and the accelerator are on [Android](https://docs.bithuman.ai/platforms/android).

## The code that matters

The render path of `MainActivity.kt`, as it is in the repository: set the secret, fetch the avatar, feed the speech, pull frames until the tail is out.

```kotlin
// excerpt: android/expression2-hello/app/src/main/java/com/example/x2hello/MainActivity.kt
Expression2Credential.set(secret)
// …
// Blocks on the network the first time; that is why this is a worker thread.
val model = Expression2ModelStore(this).fetch(agentCode)
// …
val avatar = Expression2Avatar.create(this, model, options)
// …
avatar.use {
    val frame = avatar.newFrameBitmap()   // ARGB_8888, 416 x 720 — allocate once
    avatar.feed(pcm)                      // renders each complete 1.6 s chunk
    avatar.flushTail()                    // the padded tail is the last sentence
    while (true) {
        if (avatar.pull(frame) != null) {
        // …
        }
        if (!avatar.hasPendingTail && avatar.queuedFrames == 0) break
        // A null is "not ready yet". pull() never renders, so asking again at
        // once just burns a core the engine needs — wait, then ask again.
        Thread.sleep(10)
```

The complete file is [on GitHub](https://github.com/bithuman-product/bithuman-examples/blob/main/android/expression2-hello/app/src/main/java/com/example/x2hello/MainActivity.kt).

## Make it your own

- **Your own avatar:** create one with the [Agents API](https://docs.bithuman.ai/api/agents) (`"model": "expression-2"`), then pass its agent code to `fetch`; the secret you set with `Expression2Credential.set` downloads it.
- **Live speech:** feed microphone audio (16 kHz mono float from `AudioRecord` with `ENCODING_PCM_FLOAT`) as it arrives and pull frames at 20 fps; call `flushTail()` at the end of each reply.
- **Ship it:** the secret in `BuildConfig` can be read out of the APK. A real app fetches it from your backend at startup and passes it to `Expression2Credential.set`.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `Expression2Exception` from `create()` naming the API secret | set `bithuman.apiSecret` in `local.properties`, rebuild |
| `UnsatisfiedLinkError` | run on a physical arm64 phone, not an emulator |
| The build refuses the JDK | use JDK 17 (`java -version`) |
| The app says `speech.wav` is missing | run the `adb push` line, then restart the app |

More on [Android: Troubleshooting](https://docs.bithuman.ai/platforms/android#troubleshooting).

## Next

- [Android example: Essence 2](https://docs.bithuman.ai/examples/android-essence-2) · [Android SDK](https://docs.bithuman.ai/platforms/android) · [Android API reference](https://docs.bithuman.ai/platforms/android/reference) · [source on GitHub](https://github.com/bithuman-product/bithuman-examples/tree/main/android/expression2-hello)
