# Mimi for Android Floating translated subtitles for audio playing on your phone. Watch a video, stream, or class with the original text, translation, or both on screen. [English](README.md) · [简体中文](README.zh-CN.md) · [Desktop home](../README_EN.md) ## Start here 1. [Download the Android APK](https://github.com/yuxino/mimi/releases/latest) and install it on Android 10 or later. 2. Open **Services → Add**, choose a service, enter its credentials, and save. The first configuration is selected automatically; tap a saved configuration to switch later. The [service setup guide](../docs/provider-setup.md#choose) explains where to get them; cloud usage may cost money. 3. In **Captions**, choose the spoken and translation languages, then start. Allow display over other apps, audio recording, and the system capture prompt when asked. 4. Switch to the app you want to hear and play speech. Subtitles appear over it; use Immersive Mode to keep just the text and pass touches through. Android captures app playback, **not the microphone**. Android still requires the audio-recording permission for playback capture. Some apps block capture, so protected content may produce no subtitles. Cloud recognition receives the captured audio; remote text translation receives recognized text. New installs retain the three most recent confirmed captions in memory; existing installations keep their previous setting. Stopping clears these captions without saving a transcript. Need to change the text translator? See [independent text translation](#independent-text-translation). Android has no Apple Speech or custom recognition setup. See [known limits](#known-limits) for other differences, or [report an issue](https://github.com/yuxino/mimi/issues).
Feature details and implementation Native Kotlin UI and audio capture use the shared Rust subtitle and provider runtime through JNI. Credentials are configured separately on Android. Shared code does not prove availability on every device; see [verification status](#verification-status). ## Features - **Main interface** — Captions, Services and More share one page shell and a persistent status/Start/Stop footer. Captions contains language choices, one live/sample preview, display mode and immersion. Width, font, color and opacity are directly adjustable beside the pinned preview on that same page. Services owns service selection and setup; More contains interface language, diagnostics and the setup guide. Switching sections retains their scroll positions. - **System-audio capture** — `MediaProjection` + `AudioPlaybackCapture` (API 29+). Requires Android’s `RECORD_AUDIO` runtime grant for playback capture; never uses a microphone source. Captures what other apps play (`USAGE_MEDIA/GAME/UNKNOWN`), 48 kHz stereo float, box-filter downmix + resample to the provider's target rate (16 kHz / 24 kHz mono PCM16). - **Services** — Alibaba Cloud DashScope, OpenAI Realtime Translation, Google Gemini Live, Azure OpenAI, Volcano Engine Doubao, Tencent Cloud, Baidu realtime translation and xAI Grok Voice. Their production clients are shared with desktop. Grok is turn-based voice translation; generated audio is discarded. Each service has its own encrypted credential fields and supported language choices. - **Configuration** — save multiple named configurations, including more than one for the same service. Each keeps its own credentials and text translator. Tap a configuration to select it; Settings edits it without switching. Duplicate and Delete are in the editor’s More menu. Stop captions before changing configurations. Azure requires a resource endpoint and translation/transcription deployment names; Tencent requires AppID, SecretID and SecretKey; Baidu requires AppID and AppKey. Other services use an API key. Built-in service models, endpoints and language capabilities follow the shared factory. Stored secrets are not filled back into the editor; leaving a secret field empty preserves its saved value. Changes are written only with Save; backing out of a new editor adds nothing. Existing service settings migrate automatically. - **Subtitle overlay** — one `TYPE_APPLICATION_OVERLAY` reading panel with current source and translation. Drag its top handle to move it and its corner to resize width and height. Confirmed lines appear only when bounded history is enabled; history scrolls inside the chosen height. Main and floating controls offer original/translation/bilingual display, left/center/right alignment, width (55–100 %), height (20–70 % of the safe viewport), background alpha (0–90 %), text color presets, font size and whole-window opacity. Quick settings changes update the actual captions visibly above the editor. The centered provider entry shows service brands; tap it to open Services and inspect model details. Narrow panels place it on a separate row above immersion and resize actions. Saved background and opacity apply from the first frame; changing font size preserves them. Before speech and during a silent gap, an active session shows the same localized listening message as desktop, using the selected typography and color. An optional immersive mode shows lighter typography and a compact contrast shadow, and passes touches through to the app below. A small exit control follows the subtitle position; The Captions page also enters/exits immersive mode without stopping the session. Returning restores the normal panel's saved size and position. Floating captions temporarily hide while Mimi's own pages are open; Captions shows the current caption and capture continues. Leaving Mimi restores the floating window. Immersive captions pass touches through; the normal reading panel receives touches within its covered area. - **Subtitle previews** — the shared factory selects complete paired previews for Audio3 + text translation and sentence-identified realtime previews for integrated live clients. New sentence identities never borrow old final lines; the current complete pair remains available independently of saved history. - **Alibaba Cloud** — choose simultaneous translation or ASR + MT. New configurations start with simultaneous translation; existing ones keep ASR + MT. Both modes use the desktop runtime. Follow the shared [Alibaba setup guide](../docs/provider-setup.md#alibaba-livetranslate) for workspace and key requirements. ASR + MT uses Audio3 with Qwen-MT Lite/Flash/Plus or an independent text translator; Lite is the default. Historical built-in custom endpoint/model/hotword keys are retained without enabling unsupported overrides. - **Credentials** — EncryptedSharedPreferences (Android Keystore master key), stored separately from profile metadata and isolated per configuration. - **Subtitle history** — new installations keep the three most recent confirmed captions in memory by default. Existing installations keep their previous setting, including the former zero default. Set the count to zero or stop the session to clear retained subtitles; this does not save a transcript to disk. - **Interface language** — More offers Follow system and the same eight interface languages as desktop: 简体中文, 繁體中文, English, 日本語, Deutsch, 한국어, Français and ภาษาไทย. Changes apply immediately and persist after restart, independently of subtitle source and target languages. Android 13+ also exposes this choice in system App languages. Unsupported system languages fall back to English. See the [native Before/After comparison](https://github.com/yuxino/Mimi/pull/211) for the empty-overlay, live font-size and help-icon repairs. Optional explanations appear on mouse hover/long press; tap a help icon to read scrollable details. Setup, permission and audio-recovery instructions remain visible.
## Requirements - Android 10+ (API 29); the original contributor reported Android 17 testing, and the updated interface is checked on an Android 15 emulator - Credentials for one supported service (usage may incur provider charges) - The "Display over other apps" and `RECORD_AUDIO` grants, plus per-start MediaProjection consent ## Build ```sh export ANDROID_HOME=/path/to/android-sdk rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;27.2.12479018" ./gradlew testDebugUnitTest lintDebug assembleDebug # app/build/outputs/apk/debug/app-debug.apk ``` Requires Rust 1.88+, Python 3, JDK 17, Android SDK platform 35, build-tools 35.0.0 and NDK 27.2.12479018. Gradle builds the actual shared Rust core for JVM tests and all four packaged Android ABIs; a missing native dependency fails the build. APK verification checks 16 KB ELF/ZIP alignment and license notices. See [shared JNI build](../shared/mimi-android-jni/README.md) for the state boundary, toolchain pins and cache paths. The wrapper pins Gradle 8.10.2 and verifies its distribution checksum. minSdk 29, targetSdk 35, Kotlin 2.0, AGP 8.7. CI tests/lints both variants and produces a debug APK and an unsigned release APK. The unsigned artifact is for signing, not installation; device capture remains a separate manual check. Keep the same debug signing keystore when updating an existing development installation. An alternate `ANDROID_USER_HOME` can select a different debug key. Before `adb install -r`, compare the installed APK's certificate with the new APK using `apksigner verify --print-certs`. If Android reports `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, use the existing matching debug key; do not uninstall the configured app to bypass the mismatch. Preserve its encrypted credentials and settings. Release signing remains separate. CI debug certificates are disposable. For repeated local acceptance, sign both the debug app and its instrumentation APK with the same existing local debug keystore, verify their certificates, and keep that identity for later updates. The `.dev` app ID alone does not provide signing continuity. Never use a release key for these debuggable packages. ### Signed release `version.properties` is the Android version source. Increment `versionCode` for every public update, and keep `versionName` consistent with the release notes. Build with `./gradlew testReleaseUnitTest lintRelease assembleRelease`, then run `bash sign-release.sh` with these environment variables set securely: - `ANDROID_HOME`: Android SDK directory. - `ANDROID_KEYSTORE_PATH`: existing, securely backed-up release keystore. - `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`: its signing credentials. - `ANDROID_SIGNING_CERT_SHA256`: the expected public release certificate fingerprint. The script verifies the compiled package/version and rejects debug builds, aligns and signs the APK, then verifies its signature against the pinned non-debug certificate. It writes `release/mimi__android.apk` and `release/SHA256SUMS.txt`. It never creates a temporary replacement identity. For GitHub publishing, configure repository secrets `ANDROID_KEYSTORE_BASE64` (base64 of that same keystore), `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS` and `ANDROID_KEY_PASSWORD`, plus repository variable `ANDROID_SIGNING_CERT_SHA256`. Push a shared `v` tag after staging the signed macOS assets as documented in `docs/development/macos-release-signing.md`. The main release workflow calls `android-release` to test, lint, sign and verify an APK, then waits for Android and every desktop package before publishing one complete release. Android's `versionName` must match the desktop version; its APK is included in the shared `SHA256SUMS.txt`. Existing public assets are never overwritten. The reusable Android workflow cannot publish a separate release. Keep the signing key and its backup outside Git. Use the same key for future updates. The debug app has a separate `.dev` package and can remain installed alongside the release app. Preserve each app's settings and credentials. The APK uses release build settings; this does not imply physical-device or all-provider validation. ## Local UI preview The Android interface reuses Mimi's existing character artwork and neutral light/dark palette. Subtitle source and target languages can be changed directly on the home screen with Undo. Tapping the subtitle sample opens appearance settings, where changes are saved automatically and previewed without capture or a network session. The appearance screen also previews normal and immersive text. The service tab lists saved configurations. Add opens the service picker; each configuration has a separate editor. Credentials require explicit Save; changing appearance never saves credentials. Your own service credentials are still required for real translation. The main page groups Captions, Services and More. Captions offers live original/translation/bilingual display, alignment, width, height, font, color and opacity controls. Its preview stays visible while styles scroll, above the controls in portrait and alongside them on wide screens. The floating style dialog has controls only; the existing caption window stays visible above it and responds to edits. Direct color choices wrap to retain 48dp touch targets. The main preview keeps one latest line per language in a stable viewport, so adjusting width or size does not move the controls under your finger. Full current captions remain available in the normal reading panel. Sessions open that panel unless immersive mode is enabled. Its reading viewport is bounded by the selected height, and history does not enlarge it. The panel has drag and two-dimensional resize handles, quick styles and a labeled immersive action. Display choices do not change recognition or translation requests. A running session keeps service editing locked with an explicit Stop action. The debug app uses `app.yuxino.mimi.android.dev` (mimi Dev), with independent credentials and permissions, and installs alongside the release app. Never uninstall production to install a debug APK. On an idle development emulator: ```sh ./gradlew assembleDebug assembleDebugAndroidTest adb install -r app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk adb shell am instrument -w -e theme light app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation adb shell am instrument -w -e theme dark app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation adb shell am instrument -w -e overlay_preview true -e theme light app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation adb shell am instrument -w -e overlay_interaction true app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation adb shell am instrument -w -e interface_language true -e theme light app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation adb shell am instrument -w -e overlay_listening true -e locale zh-Hans app.yuxino.mimi.android.dev.test/app.yuxino.mimi.android.UiSmokeInstrumentation ``` The interface-language check requires a blank, idle emulator. It exercises the native picker, settings recreation and localized overlay controls without capture or provider requests. Editor screenshots use the normal own-page caption hiding; overlay screenshots use a separate debug-only host without requesting capture. Use `-e leave_language zh-Hans`, force-stop the app, then run with `-e interface_language true -e expected_language zh-Hans` to check persistence. On API 33+, `overlay_listening` checks service resources against a deliberately different context locale, the normal native waiting panel, stable history viewport, immersive restoration and return from all settings sections. Use `-e locale system` for System. It uses synthetic session state, starts no capture or provider and restores the app locale and changed preferences. Include narrow/large-font and wide native layouts. The checks exercise quick language selection/Undo, direct appearance access, auto-save with actual touch gestures, all eight service editors, write-only secret fields, rejected incomplete configurations, history clearing and the keyboard. Changed non-secret preferences are restored in a finally block; provider drafts are discarded without Save. No provider or audio capture session starts. Screenshots contain sample subtitles and empty key fields and are written to the app's external `files/ui-preview` directory. Pass `-e overlay_interaction true` on a blank idle device for actual two-dimensional resize/drag, live quick settings without a duplicate preview, all alignments, stable history height, both provider badge cases and repeated immersive restoration. Quick settings keep the full live caption frame, including its route, provider badges, controls and current status/content. Those caption actions and gestures are decorative during editing; closing restores the same window. Limited space scales the complete frame rather than hiding its toolbar/footer. The `interaction-*.png` screenshots show the actual panel and edits. On a blank, idle API 35 device, `-e preview_parity true` compares both preview entries with the native frame across all eight languages plus System, waiting, no-sound/connection feedback, long captions, history and appearance endpoints. Use `-e locale ja` (or another supported tag) for one language. Normalized pixels, wrapping and frame geometry must match; quick editing preserves frame pixels. `overlay_preview` also captures long-caption reading and restored normal mode. These are synthetic native fixtures, not a real provider or audio latency measurement. Pass `-e demo true` for a paced walkthrough suitable for emulator screen recording; it demonstrates the labeled sample, not live translation. ## Independent text translation In Services → Alibaba Cloud, **Text translation** offers the built-in translation, **DeepL**, **DeepLX**, **ChatMock**, **OpenAI-compatible API**, and **No translation (original only)**. Independent modes use Alibaba Cloud's `qwen3-asr-flash-realtime` for recognition; the selected text service translates confirmed text. Original-only mode makes no text translation request. Choose **ASR + MT** in the Alibaba configuration and enable that ASR model for its key. Other speech providers keep their built-in translation. This is not an OpenAI Realtime endpoint override. - **DeepL** needs a separate DeepL API key. Mimi chooses the official Free endpoint for keys ending in `:fx`, otherwise the Pro endpoint. No custom URL or model is needed. - **DeepLX** needs your service address and an optional Bearer token. Mimi appends `/translate` when missing; the service must accept `text`, `source_lang`, `target_lang` and return integer `code: 200` with translated text in `data`. - **ChatMock** starts with `http://127.0.0.1:8000/v1`. Enter a model ID from your running server; Mimi does not choose a model or enable local HTTP for you. - **OpenAI-compatible API** takes your service endpoint, model and optional key, with no ChatMock address prefilled. Each translator retains its own encrypted configuration. The old combined entry remains **OpenAI-compatible API**, including its selected state, endpoint, model and key. Nothing is copied to the new ChatMock entry or inferred from a localhost URL. Select ChatMock and configure it separately to use the new entry. Run [ChatMock](https://github.com/RayBytes/ChatMock) yourself and enter its `/v1` base URL (or full `/v1/chat/completions` URL), a model ID from its `/v1/models`, and an optional Bearer key if your reverse proxy requires one. The endpoint must accept non-streaming Chat Completions (`model`, `messages`, `stream: false`) and return a final `choices[0].message.content`. Recommended ChatMock options are `--reasoning-compat legacy --reasoning-summary none`; Mimi also removes complete leading `` blocks and rejects missing final text. The local address describes where the ChatMock API runs. Translation models are still called online through the account signed in to ChatMock; they do not run on the phone or computer. Use HTTPS for computers and servers, including LAN addresses. With explicit local HTTP permission, only `localhost`, `127.0.0.1`, `[::1]` and Android emulator host `10.0.2.2` are supported. On a phone, localhost is the phone itself. For USB development, `adb reverse tcp:8000 tcp:8000` can make the computer's local ChatMock service available at `http://127.0.0.1:8000/v1` on the phone. Do not use a ChatGPT password or session token as Mimi's API key; account login stays in your ChatMock installation. **Test translation** sends a fixed example through the same HTTP path as subtitles and shows elapsed milliseconds. It does not save the draft or send system audio. **Save** commits both stages; back discards changes. Translation keys have independent encrypted storage and are not reused when the destination changes. Help icons open the requirements without persistent explanatory paragraphs in the editor. The final-only translation queue uses the shared Rust policy: one active request, three waiting requests, up to three attempts and a 45-second total deadline including waiting and retries. Explicit stop drains accepted work within finite shared grace windows; errors and permission revocation abort. Overflow rejects new work and drains accepted work. Late results cannot enter a newer session. These checks do not prove a live DeepL, DeepLX or ChatMock account or physical Android device until those are tested separately. ## Architecture ``` app/src/main/java/app/yuxino/mimi/android/ MainActivity.kt start flow, overlay-permission gate, projection consent SettingsActivity.kt service list and appearance settings AppearanceControls.kt shared native Settings/floating appearance controls ServiceSettingsActivity.kt provider-specific write-only credential editor ServiceSettingsUi.kt compact service picker SettingsStore.kt EncryptedSharedPreferences-backed settings capture/MimiService.kt foreground service (mediaProjection type): capture loop + overlay window + auto-hide timers resample/StreamResampler.kt O(1) box-filter downmix + resample to PCM16 mono provider/ServiceCatalog.kt service fields, capabilities and language normalization provider/SharedRuntimeEngine.kt production JNI session engine provider/ProviderEngine.kt native session interface provider/SharedSubtitleCore.kt stateless JNI bridge to shared/mimi-core provider/SubtitleBus.kt native snapshot facade over the shared Rust reducer ``` Provider clients and scheduling live in `shared/mimi-runtime`, reached through `shared/mimi-android-jni`. Historical Kotlin transport/protocol classes remain offline comparison fixtures; production capture never selects them. Run `python3 -B scripts/check-shared-runtime.py` to verify this boundary. ## Verification status The updated interface and Alibaba Cloud system-playback translation have been exercised in an Android 15 emulator with Firefox. The bundled Chrome tested there explicitly disables playback capture and yields silent samples; this finding is specific to that browser package. The additional provider adapters have protocol tests, including Tencent signing and Volcano binary frames, but have not been verified against live accounts. Do not interpret build or protocol-test success as physical-device or all-provider acceptance. The original contributor’s reported device test applies to the original port, not every subsequent change. For real playback acceptance, use the independent debug app with a stable local signing identity. `LiveAcceptanceChecks` requires native Start and fresh platform consent; its bounded report stores counts and status, not subtitle content. An explicitly authorized temporary provider fixture must enter the app's private cache through stdin, be read back before testing, and be consumed and restored in the test's finally block. Never put credentials in arguments, environment variables, screenshots, logs or Git, or treat this fixture as configuration sync. Disable video autoplay and verify the player's actual 0:00 and end states. The tested Firefox MediaSession can retain a stale position, so timestamps derived from it do not establish playback progress. Use visible player times together with continuous native observations. Do not run a separate `uiautomator dump` while instrumentation owns UiAutomation; use its own screenshots or a read-only ADB screencap. Native visual fixtures, emulator frame statistics and a live provider run establish different evidence; record their exact build and remaining device/provider gaps in the [integration ledger](../docs/development/integration-runs.md). `live_caption_reading=true` also accepts `screen_changes=true` with `phase_seconds=90` or longer. Each display mode checks landscape, immersion, small/large dimensions and exact portrait restoration while actual audio and caption drawing continue. Add `browser_fullscreen=true` for operator-controlled player fullscreen entry/exit instead of forced display rotation: respond to `CAPTION_FULLSCREEN_REQUEST` using the observed native player controls. Keep glyph and visible-rectangle comparisons in the same root-window coordinate system; screen offsets of floating windows must not become clipping failures. Browser media keys alone do not prove audio, and zero-duration injected taps can miss Firefox controls; verify the player after a native short press. ## Known limits - MediaProjection requires user consent on every start (platform rule). - DRM-protected output and apps that disallow playback capture cannot be captured. - Provider VAD can hold sentences open when background music is continuous; complete current subtitles remain readable until replaced or the session ends. - OpenAI and DashScope now use the desktop shared alignment implementations. Their provider models and other adapters still require separate live-session validation. - DashScope sentence finals depend on server-side VAD; continuous speech with music may delay whole-utterance finals. Complete text within the shared byte limit is retained. MIT, same as upstream. Native capture and UI remain Kotlin; subtitle rules, DashScope/OpenAI alignment and final translation policy are shared with desktop Rust.