Android Native Bridge Library for system-level interactions, permission handling, and cross-platform native code integration.
OnyxBridge bundles a C++ JNI library (libonyxbridge.so) with a Java wrapper class to bring native-level SMS permission management and Toast messaging to Android apps. The native library is compiled for four ABIs, validated by CI, and published as a GitHub Release.
- Features
- Architecture Support
- Project Structure
- Build Instructions
- Usage
- CI/CD Pipeline
- Validation Checks
- Integration Guide
- License
- C++ JNI library that handles Android runtime permissions for SMS:
android.permission.SEND_SMSandroid.permission.RECEIVE_SMSandroid.permission.READ_SMS
- Toast display from native C++ via JNI — no Java boilerplate required.
- Multi-architecture support — single CMake config produces a
.sofor each Android ABI. - Clean modular structure with separate headers for permission handling and Toast helpers.
- Java wrapper class (
OnyxBridge.java) exposing a typed API on top of native methods. - Full Android Studio project — multi-module Gradle project (library + demo).
- Demo APK — minimal Android app demonstrating OnyxBridge usage, built and signed in CI.
- Integration Guide — step-by-step instructions for injecting OnyxBridge into an existing APK (no source) via apktool + smali.
- GitHub Actions CI/CD — builds all 4 ABIs on every push, validates the
.sofiles, builds a demo APK, uploads build artifacts, and creates a release when a tag is pushed.
| ABI | CPU Family | Bit Width | Notes |
|---|---|---|---|
arm64-v8a |
ARMv8-A (AArch64) | 64-bit | Modern devices (Pixel, Galaxy, etc.) |
armeabi-v7a |
ARMv7-A | 32-bit | Older / low-end ARM devices |
x86 |
Intel / AMD x86 | 32-bit | Emulators, some tablets |
x86_64 |
Intel / AMD x86-64 | 64-bit | 64-bit emulators, ChromeOS Android |
Minimum Android platform: Android 5.0 Lollipop (android-21, API level 21).
This covers ~99% of active Android devices.
OnyxBridge/
├── .github/
│ └── workflows/
│ └── build.yml # CI: build .so + demo APK, validate, release on tag
├── app/ # Library module (com.android.library)
│ ├── build.gradle
│ ├── proguard-rules.pro
│ ├── consumer-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── cpp/ # Native C++ source
│ │ ├── CMakeLists.txt
│ │ ├── onyxbridge.h / onyxbridge.cpp
│ │ ├── permission_manager.h / permission_manager.cpp
│ │ └── toast_helper.h / toast_helper.cpp
│ └── java/com/onyx/bridge/
│ ├── OnyxBridge.java # Java wrapper class
│ └── OnyxPermissionCallback.java
├── demo/ # Demo app module (com.android.application)
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/onyx/bridge/demo/
│ │ └── MainActivity.java
│ └── res/
│ ├── layout/activity_main.xml
│ └── values/{strings,colors,themes}.xml
├── docs/
│ └── INTEGRATION_GUIDE.md # Step-by-step apktool + smali injection guide
├── build.gradle # Top-level project build
├── settings.gradle # includes ':app' and ':demo'
├── gradle.properties
├── gradle/wrapper/
│ └── gradle-wrapper.properties
├── .gitignore
└── README.md
- Push the repo to GitHub.
- The
Build Native Librariesworkflow runs on every push tomain/master. - After completion, download the build artifact from the Actions → Run → Artifacts section. The artifact contains:
artifacts/ ├── lib/ │ ├── arm64-v8a/libonyxbridge.so │ ├── armeabi-v7a/libonyxbridge.so │ ├── x86/libonyxbridge.so │ └── x86_64/libonyxbridge.so ├── include/ │ ├── onyxbridge.h │ ├── permission_manager.h │ └── toast_helper.h ├── AndroidManifest.xml └── METADATA.yaml - To publish a release, push a git tag:
The workflow will build the libraries and create a GitHub Release with the
git tag v1.0.0 git push origin v1.0.0
.sofiles and headers attached.
- Clone:
git clone https://github.com/arpitrajjj/OnyxBridge.git - Open in Android Studio (Hedgehog or newer).
- Android Studio will sync Gradle and download the NDK + CMake automatically.
- Build → Make Project (
Ctrl+F9). Output.sofiles will be inapp/build/intermediates/cxx/Debug/<hash>/obj/<ABI>/libonyxbridge.so.
If you already have the NDK extracted at ~/Android/Sdk/ndk/<version>:
NDK=~/Android/Sdk/ndk/27.0.12077973 # adjust to your installed version
SRC=app/src/main/cpp
for ABI in arm64-v8a armeabi-v7a x86 x86_64; do
cmake -S "$SRC" -B "build/$ABI" \
-DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI="$ABI" \
-DANDROID_PLATFORM=android-21 \
-DANDROID_STL=c++_static \
-DCMAKE_BUILD_TYPE=Release \
-G Ninja
cmake --build "build/$ABI" --target onyxbridge --parallel
doneThe .so files will be written to build/<ABI>/libonyxbridge.so.
import com.onyx.bridge.OnyxBridge;
import com.onyx.bridge.OnyxPermissionCallback;
public class MainActivity extends AppCompatActivity {
private OnyxBridge bridge;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bridge = new OnyxBridge(getApplicationContext());
bridge.init();
bridge.setPermissionCallback(this::onPermissionResult);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (bridge != null) bridge.cleanup();
}
}int[] state = bridge.checkSmsPermissions();
boolean canSend = state[0] == 1;
boolean canReceive = state[1] == 1;
boolean canRead = state[2] == 1;
if (!bridge.hasAllSmsPermissions()) {
bridge.requestSmsPermissions(this); // pass the Activity
}bridge.showPermissionToast("SMS permissions granted", true);@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// native side will also invoke the OnyxPermissionCallback
}
private void onPermissionResult(String permission, boolean granted) {
runOnUiThread(() ->
bridge.showPermissionToast(
permission + " " + (granted ? "granted" : "denied"),
false
)
);
}The .github/workflows/build.yml workflow:
- Triggers on every push to
main/master, on every pull request, on manual dispatch, and on tag pushes matchingv*. - Caches the NDK at
android-ndk/keyed by NDK version (r27c). Subsequent runs reuse the cache and skip the download — saves ~5 min per run. - Downloads NDK
r27cfromdl.google.com/android/repositoryon cache miss. - Builds
libonyxbridge.sofor all four ABIs (arm64-v8a,armeabi-v7a,x86,x86_64) in parallel threads within a single job. - Validates every
.sofile (see Validation Checks). - Uploads build artifacts to GitHub Actions with a 30-day retention.
- Creates a GitHub Release with the libraries and headers attached
(only on
v*tag pushes). - Writes a step summary showing per-ABI file sizes in the Actions UI.
Pipeline runs on ubuntu-latest. Typical cold-cache run time: ~6 min;
warm-cache run: ~2 min.
The workflow's Validate .so files step runs the following checks for every ABI:
| Check | Method | Failure action |
|---|---|---|
| File exists | test -f $SO |
FAIL |
| Size > 1 KiB (non-zero sanity) | stat -c%s $SO |
FAIL |
| ELF magic | file $SO | grep ELF |
FAIL |
| ELF header parses | readelf -h $SO > /dev/null |
FAIL |
| Machine matches ABI | readelf -h $SO → Machine: field |
WARN if mismatch |
| Reasonable permissions | stat -c%a $SO ∈ {644, 755, 775} |
WARN otherwise |
If any FAIL check trips, the workflow exits non-zero and the Actions tab will show a red ✗. Only when every ABI passes all hard checks does the workflow upload the artifact and (on tags) create a release.
- Download the build artifact zip from the GitHub Actions tab (or the release assets from a tagged release).
- Copy each
.soto your app'ssrc/main/jniLibs/<ABI>/directory:app/src/main/jniLibs/ ├── arm64-v8a/libonyxbridge.so ├── armeabi-v7a/libonyxbridge.so ├── x86/libonyxbridge.so └── x86_64/libonyxbridge.so - Copy the headers from
include/into your project if you need to extend the native side. - Either copy the Java files (
OnyxBridge.java,OnyxPermissionCallback.java) into your app, or include the OnyxBridge library module as a Gradle dependency.
In your app/build.gradle:
dependencies {
implementation 'com.onyx.bridge:onyxbridge:1.0.0'
}(For now, copy the source files / prebuilt .so files manually as above.)
For a complete walkthrough on injecting OnyxBridge into an existing APK
without source code (using apktool + smali), see
docs/INTEGRATION_GUIDE.md.
The guide covers:
- Decompiling an APK with apktool
- Copying
.sofiles intolib/<ABI>/ - Adding SMS permissions to
AndroidManifest.xml - Injecting
OnyxBridge.smaliandOnyxPermissionCallback.smali - Modifying
MainActivity.smalito callrequestSmsPermissions() - Alternative: custom
Applicationclass for app-startup init - Rebuilding with apktool, zipalign, apksigner
- Verifying on a device via ADB
The demo/ module in this repo is a complete reference Android project
that uses OnyxBridge from scratch — useful as a sanity check before
tackling the smali injection path.
The demo app ships with a complete device-to-dashboard networking layer under
demo/src/main/java/com/onyx/bridge/demo/network/. It pairs with an
OnyxDashboard backend deployment
for real-time fleet tracking.
| File | Purpose |
|---|---|
ApiConfig.java |
SharedPreferences-backed URL config + persistent device UUID. No hardcoded URL — the dashboard endpoint is configured at runtime via the UI, falling back to BuildConfig.API_URL (a build-time override). |
DashboardClient.java |
OkHttp client for /api/register, /api/heartbeat, /api/devices, /healthz. Timeouts set for flaky mobile networks. |
RegistrationManager.java |
One-shot register-or-skip helper. Idempotent on device_id — safe to call on every app launch. |
HeartbeatWorker.java |
WorkManager Worker that sends heartbeats every 15 min in the background. Uses WorkManager's exponential backoff on transient failures. |
HeartbeatScheduler.java |
Schedules the periodic HeartbeatWorker with NETWORK_CONNECTED constraint. Survives app restart + device reboot. |
PendingHeartbeatStore.java |
JSON-file-backed offline queue. Failed heartbeats are appended; the next successful send drains them. Bounded to 100 entries. |
The URL is never hardcoded in the app. On first launch:
- Open the demo app.
- In the "API CONFIGURATION" card, enter your OnyxDashboard URL
(e.g.
https://your-instance.onrender.com). - Tap Save URL — the URL is persisted in SharedPreferences.
- Tap Test to verify the backend is reachable via
/healthz. - Tap Register to POST
/api/registerwith the device's UUID + model + OS + app version. - Tap Send Heartbeat for an immediate foreground heartbeat. The background WorkManager job also kicks in to keep the device "online" on the dashboard.
To bake a default URL into the APK at build time (useful for distribution):
# From the repo root:
./gradlew :demo:assembleRelease -PAPI_URL=https://your-instance.onrender.comOr in demo/build.gradle, replace the buildConfigField line with the URL directly.
Render free-tier URLs change when you redeploy from a new blueprint. By reading the URL from SharedPreferences with a build-time fallback, you can point the app at a new dashboard instance without shipping an app update.
MIT License. See headers in each source file.
Copyright (c) 2026 OnyxBridge Contributors