I did not build BIT because the world needed another AI wrapper. I built it because I was exhausted. Every single "AI assistant" launching on Android in 2026 was the exact same thing: a thin HTTP client disguised as innovation, charging monthly subscriptions, and sending every keystroke, question, and private document to a remote server.
I wanted an AI that actually lived on my phone. 100% offline. Zero telemetry. No subscriptions. True multimodal reasoning, neural speech synthesis, and sandboxed code execution running entirely on the silicon inside my pocket.
As a B.Tech student at Parul University working completely solo, the stakes felt absurd. I spent six straight months writing low-level C++, debugging JNI memory leaks, and fighting the Linux kernel with no guarantee that anyone would ever install it—and worse, no guarantee that Google Play's automated review systems would ever allow an app that executes local native binaries and loads multi-gigabyte GGUF model files.
This is the unfiltered engineering story of how BIT made it from raw assembly crashes on borrowed phones to an approved production release on the Google Play Store.
The SDK: Curating and Rebuilding the Native Runtime
Running a 2-billion to 7-billion parameter language model on a server is straightforward: you throw high-bandwidth VRAM and cloud power at it. Running it on a consumer smartphone is a knife fight against hardware limits, battery thermals, and operating system watchdogs.
To make BIT viable inside real Android apps, I spent six months curating, porting, and hardening an entire low-level stack:
llama.kt & llama.cpp: I ported the native inference engine to modern Android ARM64 (
arm64-v8a) and x86_64, writing custom JNI bindings that expose asynchronous Kotlin Coroutines and reactiveFlowstreams. For Android 15 compatibility, every native library had to be aligned to 16 KB memory page sizes (-Wl,-z,max-page-size=16384) in CMake to avoid linker rejections on newer kernels.Multi-Process Binder IPC Architecture: Android's Low Memory Killer (LMK) will instantly murder any foreground app that suddenly spikes RAM usage to 3 GB. To survive this, I architected a multi-process boundary with a dedicated
:model_service(com.bit.agent.service.LLMService). The heavy GGUF tensor calculations run in an isolated Linux process, communicating with the UI via high-speed Binder IPC. If the model experiences memory pressure during a heavy context pass, the UI never drops a frame or stutters.Offline Speech & Vision: I embedded Sherpa-ONNX paired with the Kokoro neural TTS model for real-time offline voice output, Whisper for local speech-to-text, and a custom Stable Diffusion JNI pipeline for on-device image synthesis.
The PRoot Linux Subsystem: To give the assistant real tool autonomy without requiring device root, I embedded a sandboxed Alpine/Debian ARM64 user-space rootfs managed via PRoot. This lets the assistant run genuine Python scripts, data processing, and shell utilities locally on the phone.
The 3 Months of App Testing: What Broke on Real Hardware
In August 2026, I opened closed testing. In an Android emulator, everything looked pristine. The moment real testers installed the builds on consumer devices with Qualcomm, MediaTek, and Tensor chipsets, the silicon threw pure chaos at the engine.
The Qualcomm vs. MediaTek Dynamic CPU Disaster
On MediaTek Dimensity 8200 and 9200 devices, the app died immediately upon loading any model weights with an abrupt native crash:
signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0x...
[FATAL] llama.cpp: Illegal instruction in ggml-cpu backend execution
We had enabled dynamic CPU backend compilation in CMake, which compiled optimized dot-product assembly (__ARM_FEATURE_DOTPROD). On heterogeneous big.LITTLE architectures, when Android scheduled threadpool workers onto efficiency cores that lacked the specific vector extension, the CPU threw a fatal illegal instruction trap. In commits af6bbd4 and 73571eb, I had to completely revert dynamic runtime backend loading and lock down a battle-tested static ARMv8.2-A compilation baseline with runtime feature probing.
Snapdragon 7s Gen 2 Segmentation Faults
Testers running mid-range Snapdragon 7s Gen 2 devices experienced memory corruption during heavy prompt ingest. It took days of tracking memory offsets and cache flushing before fixing it in commit 1996395.
The Release-Mode Threadpool SIGBUS
Debug builds ran flawlessly. But the moment we generated Release builds with compiler optimizations, the threadpool matrix multiplication crashed with SIGBUS on certain ARM cores due to 64-bit atomic struct alignment mismatches. I had to restructure the native thread memory layouts and add strict memory barriers (v1.6.8).
R8 Stripping Native Symbols
ProGuard and R8 optimization silently stripped Kotlin data class bindings used across the JNI bridge for ONNX Runtime and Sherpa. At runtime, Release builds exploded with:
java.lang.UnsatisfiedLinkError: No implementation found for long com.bit.agent.native.OnnxRuntime.OrtGetApiBase()
I had to author exhaustive ProGuard keep rules for all native bindings and dynamic reflection classes.
Tester Bug Triage (Issues #13 - #16)
In commit 7099f82, I addressed four major tester-reported issues: sub-1B models crashing due to prompt context overflow (implemented sliding-window context truncation), thinking UI spinners freezing during deep reasoning streams, unhandled StableDiffusion VRAM exhaustion, and Qualcomm DSP driver timeouts requiring seamless fallback to CPU execution.
The Submission Crucible: Chronological Battles
When the engine finally stabilized on real hardware, I thought the hardest part was over. I was wrong. The Google Play submission process was an obstacle course of automated rejections, policy audits, and build collisions.
1. Application ID Namespace Collision (2026-09-07)
Commit: 8c1b1b3
The Error:
Upload failed: Package name "com.bit" is already in use by another application or reserved by Google Play.
What didn't work: I looked into namespace appeals, but generic three-letter packages like com.bit are either reserved by legacy apps or permanently blocked by Google Play. There was no appeal process.
The Fix: I had to refactor the entire package namespace across the project to com.bit.agent. This was not a simple string replace. It meant rewriting app/build.gradle.kts, AndroidManifest.xml, room database authorities, FileProvider XMLs, and rewriting every single C++ JNI method signature (changing Java_com_bit_... to Java_com_bit_agent_...) across all native source files.
What I'd do differently: Reserve and register your exact package identifier on Google Play Console before writing your first native JNI method.
2. High-Risk Permission Rejection: REQUEST_INSTALL_PACKAGES (2026-09-07)
Commit: 8c1b1b3
The Policy Rejection:
Policy Violation: Your app requested 'android.permission.REQUEST_INSTALL_PACKAGES' but does not qualify as an alternative app market or meet permitted enterprise use cases.
What didn't work: In early alpha testing, BIT included an in-app updater that polled GitHub Releases, downloaded fresh APKs, and prompted the tester to install them. I submitted an explanation arguing that it was a user-initiated update for testers. The automated policy scanner rejected it immediately.
The Fix: In commit 8c1b1b3, I completely purged REQUEST_INSTALL_PACKAGES from the manifest. For Play Store builds, app distribution must go through official store channels; for standalone open-source builds, updates redirect users directly to browser release downloads.
What I'd do differently: Isolate open-source self-updating mechanisms into dedicated Gradle product flavors (play vs github) from the very first commit.
3. AAB Build Failure with ABI Bundle Splits & CMake Target Collision (2026-09-07)
Commit: 8c1b1b3
The Build Error:
Execution failed for task ':app:bundleReleaseClasses'.
> Cannot enable ABI splits when building an Android App Bundle (AAB).
And immediately following that, CMake failed:
CMake Error at CMakeLists.txt: Target "cpp-httplib" has already been declared by external/whisper.cpp/CMakeLists.txt.
What didn't work: I tried uploading universal and split APKs to the Play Console. Google Play rejected them outright because all newly created apps must be published as Android App Bundles (AAB).
The Fix: In app/build.gradle.kts, I made ABI splits conditional so they only run when building standalone APKs and automatically disable themselves during bundle generation:
val isBuildingBundle = gradle.startParameter.taskNames.any { it.contains("bundle", ignoreCase = true) }
splits {
abi {
isEnable = !isBuildingBundle
reset()
include("arm64-v8a", "x86_64")
isUniversalApk = false
}
}
In CMake, I added include guards around common dependencies (if(NOT TARGET cpp-httplib) add_subdirectory(...) endif()) to eliminate duplicate target declarations between the main app and whisper.cpp.
What I'd do differently: Add ./gradlew bundleRelease to your continuous integration pipeline early instead of only testing assembleRelease.
4. Play Policy Compliance Audit: PRoot Linux Rootfs Provisioning (2026-09-09)
Commit: 915a66d
The Policy Flag:
Issue found: Violation of Device and Network Abuse policy.
Apps must not download executable code (e.g. dex, JAR, native binaries) from external sources that can alter app behavior beyond what was submitted for review.
Context: To keep BIT's initial download size reasonable, the app downloaded its user-space Alpine Linux rootfs archive during first-time setup so users could run Python and local tools. Google's review system flagged this as dynamically downloading executable machine code.
What didn't work: I initially responded explaining that PRoot operates entirely in unprivileged user space via ptrace system call virtualization and cannot escalate Android OS privileges. The appeal was rejected because the process lacked verifiable user consent and cryptographic integrity guarantees.
The Fix: In commit 915a66d, I executed a complete compliance overhaul:
Implemented
RootfsDownloadConsentDialog.kt, presenting an explicit user prompt disclosing the exact archive size, storage requirements, and sandboxed execution model before any network transfer begins.Enforced hardcoded cryptographic SHA-256 verification against the downloaded archive before extracting a single byte.
Isolated Python pip packages strictly to an internal
/opt/bit-envdirectory.Drafted
docs/PLAY_ASSET_DELIVERY.mdand updated store listings to document the architectural migration toward official Google Play Asset Delivery (PAD) install-time asset packs.
What I'd do differently: Treat any user-space execution environment as a high-risk security surface from day one: never download executable assets without cryptographic checksums, explicit user consent dialogs, and clear store listing disclosures.
5. Non-Play Self-Update Policy Violation (2026-09-12)
Commit: a60f98e
The Policy Rejection:
Google Play Developer Policy strictly prohibits apps installed from Google Play from checking or directing users to third-party update channels (such as GitHub Releases).
What didn't work: Hiding the update check button inside an "Advanced Developer Settings" menu. Google's static bytecode analysis still flagged the GitHub API update check strings inside the compiled classes.
The Fix: In commit a60f98e, I implemented strict runtime installer detection:
val installer = context.packageManager.getInstallSourceInfo(context.packageName).installingPackageName
val isInstalledFromPlay = installer == "com.android.vending"
if (isInstalledFromPlay) {
// Disable GitHub release check; use official Google Play In-App Updates API
}
What I'd do differently: Always condition your update check logic on the application's installer source.
The Day It Got Approved
After twenty dedicated testers completed the mandatory 14-day continuous closed testing phase on Google Play Console, I promoted build #82 (v2.1.1) to the production track.
For several days, the console status read: "In review." Every morning, I woke up checking my email with that familiar sinking feeling, bracing for another policy rejection or automated flag.
And then, the notification finally landed:
Update status: Production: Available to all users.
[JASWANTH: What exact day and hour did you see the "Your app is live on Google Play" email?]
[JASWANTH: Where were you on campus or at home when you opened the Play Console and saw "Production: Available to all users"?]
After six months of feeling like I was screaming into an empty room—debugging C++ segmentation faults in campus labs between lectures while everyone else was building standard cloud web apps—BIT was live on the Google Play Store. Real users across the world could download a 100% private, on-device AI assistant with a single tap.
Checklist for Solo Developers Shipping Offline AI to Play
If you are building an on-device AI or low-level native Android application, here is the survival checklist you need before touching the Play Console:
1. Reserve your package name immediately: Register your application ID on Google Play Console before writing any JNI method signatures.
2. 16 KB memory page compliance: Add
-Wl,-z,max-page-size=16384to your CMake flags now. Android 15 requires this for native libraries.3. Multi-process Binder separation: Never run heavy inference on the main UI process. Run your GGUF/ONNX engines in an isolated
:serviceto prevent Low Memory Killer terminations.4. Guard ABI splits in Gradle: Automatically disable
splits.abi.isEnablewhen building Android App Bundles (AABs).5. Guard runtime downloads: If downloading model weights or rootfs archives post-install, implement explicit user consent dialogs, hardcoded SHA-256 integrity verification, and disclose it in your Data Safety form.
6. Verify installer source: Deactivate external update checkers if
installingPackageName == "com.android.vending".7. Exhaustive ProGuard rules: Explicitly
-keepall JNI binding classes, native structs, and reflection interfaces to prevent R8 runtimeUnsatisfiedLinkErrorcrashes.8. Test on heterogeneous silicon: Never trust emulators. Test your builds on Qualcomm Snapdragon, MediaTek Dimensity, and Google Tensor chips to catch vector assembly traps before your users do.
Acknowledgments & Google Play Link
This milestone would not have been possible without the support of the people who believed in this project when it was just a collection of crashing terminal windows:
My 20+ Closed Beta Testers, who installed experimental builds on their personal phones, reported logcats, endured system freezes, and helped me catch critical silicon crashes before launch.
You can download BIT directly on Google Play today: