Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Theme
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Create API Key
Add Docs
JNI
https://github.com/androidgolab/jni
Admin
JNI is an idiomatic Go library providing bindings for the Java Native Interface and 53 Android Java
...
Tokens:
36,405
Snippets:
293
Trust Score:
7.5
Update:
1 week ago
Context
Skills
Chat
Benchmark
89.5
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# go-jni — Android JNI Bindings for Go `go-jni` (`github.com/AndroidGoLab/jni`) is a Go library that provides complete, type-safe access to Android Java APIs from Go code via JNI (Java Native Interface). It exposes a low-level JNI layer (`VM`, `Env`, `Class`, `Object`, `MethodID`, `FieldID`) and, on top of it, 53+ auto-generated package wrappers that each correspond to an Android system service or SDK class — `bluetooth`, `location`, `telephony`, `net/wifi`, `os/battery`, `app/notification`, `content/resolver`, and many more. Every generated package follows the same Manager pattern: create a manager from an `*app.Context`, call typed Go methods that internally run inside `VM.Do()`, and `Close()` the manager when done. The library is designed for use with any Go Android framework — Gio (`app.JavaVM()`), gomobile (`RunOnJVM`), or plain NativeActivity (`ANativeActivity.vm`). It handles JNI thread attachment, local/global reference promotion, Java exception conversion to Go errors, method-ID caching, and both interface callbacks (`env.NewProxy`) and abstract-class callbacks (`GoAbstractDispatch` + `RegisterProxyHandler`). Constants for all Android SDK values are re-exported as named Go identifiers in each package and in a `consts` sub-package. --- ## Core JNI Layer ### VM — JavaVM wrapper and entry point `VM` wraps a `JavaVM*` pointer. It is process-global and thread-safe. `VM.Do()` is the single entry point for all JNI work: it locks the calling goroutine to an OS thread, attaches it to the JVM if needed, runs the closure, and detaches on return. Local references created inside the closure are invalidated when it returns. ```go import "github.com/AndroidGoLab/jni" // Obtain a VM from a NativeActivity: // vm := jni.VMFromUintptr(uintptr(activity.vm)) // From Gio: // vm := jni.VMFromUintptr(uintptr(app.JavaVM())) // All JNI calls must happen inside VM.Do. err := vm.Do(func(env *jni.Env) error { cls, err := env.FindClass("android/os/Build") if err != nil { return fmt.Errorf("find Build: %w", err) } fid, err := env.GetStaticFieldID(cls, "MODEL", "Ljava/lang/String;") if err != nil { return err } obj := env.GetStaticObjectField(cls, fid) model := env.GoString((*jni.String)(unsafe.Pointer(obj))) fmt.Println("Model:", model) // e.g., "Pixel 8 Pro" return nil }) if err != nil { log.Fatal(err) } ``` --- ### VMFromUintptr / VMFromPtr — VM construction Creates a `*VM` from the pointer value passed by Android frameworks. ```go // NativeActivity pattern /* #include <android/native_activity.h> */ import "C" import "unsafe" //export ANativeActivity_onCreate func ANativeActivity_onCreate(activity *C.ANativeActivity, _ unsafe.Pointer, _ C.size_t) { vm := jni.VMFromUintptr(uintptr(activity.vm)) activityObj := jni.ObjectFromUintptr(uintptr(activity.clazz)) _ = vm _ = activityObj // Alternatively from a raw pointer: // vm := jni.VMFromPtr(unsafe.Pointer(activity.vm)) } ``` --- ### Env — JNI environment and method/field calls `Env` is thread-local and is only valid inside the `VM.Do()` closure where it was obtained. It exposes typed call methods for every JNI primitive and object type. Java exceptions are automatically cleared and returned as Go errors. ```go vm.Do(func(env *jni.Env) error { // ---- Instance method call ---- cls, _ := env.FindClass("android/bluetooth/BluetoothAdapter") mid, _ := env.GetMethodID(cls, "getName", "()Ljava/lang/String;") resultObj, err := env.CallObjectMethod(adapterObj, mid) if err != nil { return err } name := env.GoString((*jni.String)(unsafe.Pointer(resultObj))) // ---- Static method call ---- buildCls, _ := env.FindClass("android/os/Build") smid, _ := env.GetStaticMethodID(buildCls, "getRadioVersion", "()Ljava/lang/String;") radioObj, _ := env.CallStaticObjectMethod(buildCls, smid) radio := env.GoString((*jni.String)(unsafe.Pointer(radioObj))) // ---- Constructor ---- htCls, _ := env.FindClass("android/os/HandlerThread") initMid, _ := env.GetMethodID(htCls, "<init>", "(Ljava/lang/String;)V") jName, _ := env.NewStringUTF("MyThread") defer env.DeleteLocalRef(&jName.Object) htObj, _ := env.NewObject(htCls, initMid, jni.ObjectValue(&jName.Object)) fmt.Println("name:", name, "radio:", radio, "ht:", htObj) return nil }) ``` --- ### Env Call Methods — typed method dispatch All typed `Call*Method` and `CallStatic*Method` variants wrap the JNI `Call<Type>MethodA` family. | Method | Returns | JNI | |---|---|---| | `CallBooleanMethod(obj, mid, args...)` | `uint8, error` | `Z` | | `CallByteMethod(obj, mid, args...)` | `int8, error` | `B` | | `CallCharMethod(obj, mid, args...)` | `uint16, error` | `C` | | `CallShortMethod(obj, mid, args...)` | `int16, error` | `S` | | `CallIntMethod(obj, mid, args...)` | `int32, error` | `I` | | `CallLongMethod(obj, mid, args...)` | `int64, error` | `J` | | `CallFloatMethod(obj, mid, args...)` | `float32, error` | `F` | | `CallDoubleMethod(obj, mid, args...)` | `float64, error` | `D` | | `CallObjectMethod(obj, mid, args...)` | `*Object, error` | `L` | | `CallVoidMethod(obj, mid, args...)` | `error` | `V` | ```go vm.Do(func(env *jni.Env) error { cls, _ := env.FindClass("android/telephony/TelephonyManager") // int return mid, _ := env.GetMethodID(cls, "getPhoneType", "()I") phoneType, err := env.CallIntMethod(tmgrObj, mid) if err != nil { return err } // bool return: JNI returns uint8, convert to Go bool mid2, _ := env.GetMethodID(cls, "isNetworkRoaming", "()Z") raw, _ := env.CallBooleanMethod(tmgrObj, mid2) roaming := raw != 0 // void call with arguments mid3, _ := env.GetMethodID(cls, "requestSingleUpdate", "(Ljava/lang/String;Landroid/location/LocationListener;)V") jProvider, _ := env.NewStringUTF("gps") defer env.DeleteLocalRef(&jProvider.Object) env.CallVoidMethod(mgrObj, mid3, jni.ObjectValue(&jProvider.Object), jni.ObjectValue(listenerProxy)) fmt.Println("phoneType:", phoneType, "roaming:", roaming) return nil }) ``` --- ### Field Access — GetFieldID / GetStaticFieldID ```go vm.Do(func(env *jni.Env) error { // Instance fields cls, _ := env.FindClass("android/graphics/Point") xFid, _ := env.GetFieldID(cls, "x", "I") yFid, _ := env.GetFieldID(cls, "y", "I") x := env.GetIntField(pointObj, xFid) y := env.GetIntField(pointObj, yFid) fmt.Printf("Point: (%d, %d)\n", x, y) // Static String field buildCls, _ := env.FindClass("android/os/Build") fid, _ := env.GetStaticFieldID(buildCls, "MODEL", "Ljava/lang/String;") obj := env.GetStaticObjectField(buildCls, fid) model := env.GoString((*jni.String)(unsafe.Pointer(obj))) // Static int field (Android API level) verCls, _ := env.FindClass("android/os/Build$VERSION") sdkFid, _ := env.GetStaticFieldID(verCls, "SDK_INT", "I") sdkInt := env.GetStaticIntField(verCls, sdkFid) fmt.Printf("Model: %s, API: %d\n", model, sdkInt) return nil }) ``` --- ### Reference Management — NewGlobalRef / DeleteGlobalRef / DeleteLocalRef JNI local references are only valid inside a single `VM.Do()` call. Promote to global refs when an object must outlive the closure. ```go var globalObj *jni.Object // Create a global ref inside vm.Do vm.Do(func(env *jni.Env) error { localObj, err := env.CallObjectMethod(someObj, someMethod) if err != nil { return err } // Promote to global ref — survives outside this vm.Do scope globalObj = env.NewGlobalRef(localObj) env.DeleteLocalRef(localObj) // free the local ref immediately return nil }) // globalObj is valid here and in other goroutines. // Must be freed explicitly when no longer needed: vm.Do(func(env *jni.Env) error { env.DeleteGlobalRef(globalObj) globalObj = nil return nil }) ``` --- ### String Conversion — NewStringUTF / GoString ```go vm.Do(func(env *jni.Env) error { // Go string → JNI java.lang.String jStr, err := env.NewStringUTF("hello from Go") if err != nil { return err } defer env.DeleteLocalRef(&jStr.Object) // Pass to JNI: jni.ObjectValue(&jStr.Object) // JNI java.lang.String → Go string resultObj, _ := env.CallObjectMethod(obj, someStringMethod) goStr := env.GoString((*jni.String)(unsafe.Pointer(resultObj))) fmt.Println(goStr) // "hello from Go" return nil }) ``` --- ### Value Constructors — typed JNI argument wrappers Use these when calling raw JNI methods that take `...Value` arguments. ```go jni.BooleanValue(uint8(1)) // Java boolean (0 or 1, NOT Go bool) jni.ByteValue(int8(-5)) // Java byte jni.CharValue(uint16('A')) // Java char jni.ShortValue(int16(100)) // Java short jni.IntValue(int32(42)) // Java int jni.LongValue(int64(1_000_000)) // Java long jni.FloatValue(float32(3.14)) // Java float jni.DoubleValue(float64(3.14159)) // Java double jni.ObjectValue(obj) // Java Object (pass *jni.Object) // Example: calling requestLocationUpdates(String, long, float, Listener, Looper) env.CallVoidMethod(mgrObj, mid, jni.ObjectValue(&jProvider.Object), jni.LongValue(int64(0)), jni.FloatValue(float32(0)), jni.ObjectValue(listenerProxy), jni.ObjectValue(looperObj), ) ``` --- ### env.NewProxy — implement Java interfaces in Go Creates a `java.lang.reflect.Proxy` that implements one or more Java interfaces and routes all method calls to a Go closure. Works for **interfaces only** (not abstract classes). ```go vm.Do(func(env *jni.Env) error { // Find the Java interface class listenerCls, err := env.FindClass("android/location/LocationListener") if err != nil { return err } // Create the proxy — receives calls on any method of the interface proxy, cleanup, err := env.NewProxy( []*jni.Class{listenerCls}, func(env *jni.Env, methodName string, args []*jni.Object) (*jni.Object, error) { switch methodName { case "onLocationChanged": // args[0] is a Location object (local ref) locGlobal := env.NewGlobalRef(args[0]) loc := &location.Location{VM: vm, Obj: locGlobal} lat, _ := loc.GetLatitude() lon, _ := loc.GetLongitude() fmt.Printf("Location: %.6f, %.6f\n", lat, lon) env.DeleteGlobalRef(locGlobal) case "onProviderEnabled": fmt.Println("Provider enabled") case "onProviderDisabled": fmt.Println("Provider disabled") } return nil, nil // void method }, ) if err != nil { return err } defer cleanup() // frees Go-side resources; call when listener is removed // Pass proxy to Android APIs just like any Java object _ = proxy return nil }) ``` --- ### RegisterProxyHandler / UnregisterProxyHandler — abstract class callbacks For Java **abstract classes** (e.g., `BroadcastReceiver`, `ScanCallback`, `CameraDevice.StateCallback`), `env.NewProxy` does not work. Use the `GoAbstractDispatch` bridge: write a small Java adapter that extends the abstract class and delegates to `GoAbstractDispatch.invoke(handlerID, methodName, args)`, then register a Go handler for the returned `handlerID`. ```go import ( "fmt" "github.com/AndroidGoLab/jni" "github.com/AndroidGoLab/jni/app" "github.com/AndroidGoLab/jni/bluetooth" "github.com/AndroidGoLab/jni/content" ) // Java adapter (must be compiled into the APK): // package center.dx.jni.generated; // public class GoBroadcastReceiver extends BroadcastReceiver { // private final long handlerID; // public GoBroadcastReceiver(long handlerID) { this.handlerID = handlerID; } // @Override public void onReceive(Context ctx, Intent intent) { // GoAbstractDispatch.invoke(handlerID, "onReceive", new Object[]{ctx, intent}); // } // } func listenForBluetooth(vm *jni.VM, ctx *app.Context) (func(), error) { // 1. Register the Go handler; get a stable ID handlerID := jni.RegisterProxyHandler( func(env *jni.Env, method string, args []*jni.Object) (*jni.Object, error) { if method != "onReceive" || len(args) < 2 { return nil, nil } // Promote intent to global ref so typed accessors (vm.Do) work intentGlobal := env.NewGlobalRef(args[1]) intent := app.Intent{VM: vm, Obj: intentGlobal} defer env.DeleteGlobalRef(intentGlobal) action, _ := intent.GetAction() if action == bluetooth.ActionFound { deviceObj, _ := intent.GetParcelableExtra(bluetooth.ExtraDevice) if deviceObj != nil { devGlobal := env.NewGlobalRef(deviceObj) dev := bluetooth.Device{VM: vm, Obj: devGlobal} name, _ := dev.GetName() addr, _ := dev.GetAddress() fmt.Printf("Found: %s (%s)\n", name, addr) env.DeleteGlobalRef(devGlobal) } } return nil, nil }, ) // 2. Create IntentFilter filter, err := content.NewIntentFilter(vm, bluetooth.ActionFound) if err != nil { jni.UnregisterProxyHandler(handlerID) return nil, err } // 3. Instantiate the Java adapter via raw JNI var receiverGlobal *jni.GlobalRef err = vm.Do(func(env *jni.Env) error { recvCls, err := env.FindClass("center/dx/jni/generated/GoBroadcastReceiver") if err != nil { return fmt.Errorf("find GoBroadcastReceiver: %w", err) } defer env.DeleteLocalRef(&recvCls.Object) initMid, _ := env.GetMethodID(recvCls, "<init>", "(J)V") local, err := env.NewObject(recvCls, initMid, jni.LongValue(handlerID)) if err != nil { return err } receiverGlobal = env.NewGlobalRef(local) env.DeleteLocalRef(local) return nil }) if err != nil { filter.Close() jni.UnregisterProxyHandler(handlerID) return nil, err } // 4. Register with Context ctx.RegisterReceiver2((*jni.Object)(unsafe.Pointer(receiverGlobal)), filter.Obj) // 5. Return cleanup function return func() { ctx.UnregisterReceiver((*jni.Object)(unsafe.Pointer(receiverGlobal))) vm.Do(func(env *jni.Env) error { env.DeleteGlobalRef(receiverGlobal) return nil }) filter.Close() jni.UnregisterProxyHandler(handlerID) }, nil } ``` --- ## app Package — Android Context ### app.Context — wraps android.content.Context `app.Context` wraps an Android `Context` object and provides access to system services and application-level APIs. ```go import "github.com/AndroidGoLab/jni/app" // Create from a global ref (e.g., NativeActivity.clazz global ref) ctx, err := app.ContextFromObject(vm, activityGlobalRef) if err != nil { return err } defer ctx.Close() // Get any system service by name svcObj, err := ctx.GetSystemService("battery") if err != nil { return err } defer vm.Do(func(env *jni.Env) error { env.DeleteGlobalRef(svcObj) return nil }) // Get the application context appCtxObj, err := ctx.GetApplicationContext() // Register and unregister a BroadcastReceiver _, err = ctx.RegisterReceiver2(receiverObj, intentFilterObj) err = ctx.UnregisterReceiver(receiverObj) // Get the ContentResolver resolverObj, err := ctx.GetContentResolver() ``` --- ### app.Intent — wraps android.content.Intent ```go import "github.com/AndroidGoLab/jni/app" // Wrap a raw Intent object (e.g., received in a BroadcastReceiver callback) intent := app.Intent{VM: vm, Obj: intentGlobalRef} // Read the action string action, err := intent.GetAction() // e.g., "android.bluetooth.device.action.FOUND" // Read extras by key state, err := intent.GetIntExtra("wifi_state", -1) deviceObj, err := intent.GetParcelableExtra("android.bluetooth.device.extra.DEVICE") strExtra, err := intent.GetStringExtra("some_key") // Example: decode a WiFi state broadcast if action == wifi.WifiStateChangedAction { wifiState, err := intent.GetIntExtra(wifi.ExtraWifiState, -1) if err == nil { fmt.Printf("WiFi state changed to: %d\n", wifiState) } } ``` --- ## Android API Packages — System Service Managers All 53 packages follow the same pattern: `NewManager(ctx)` → typed methods → `Close()`. ### os/battery — BatteryManager ```go import "github.com/AndroidGoLab/jni/os/battery" mgr, err := battery.NewManager(ctx) if err != nil { return err } defer mgr.Close() // Convenience bool check charging, _ := mgr.IsCharging() // Typed property queries capacity, _ := mgr.GetIntProperty(int32(battery.BatteryPropertyCapacity)) // 0-100 currentNow, _ := mgr.GetIntProperty(int32(battery.BatteryPropertyCurrentNow)) // microamps chargeCounter, _ := mgr.GetIntProperty(int32(battery.BatteryPropertyChargeCounter)) energy, _ := mgr.GetLongProperty(int32(battery.BatteryPropertyEnergyCounter)) // nWh status, _ := mgr.GetIntProperty(int32(battery.BatteryPropertyStatus)) // Compare status using named constants: // battery.BatteryStatusCharging, BatteryStatusDischarging, BatteryStatusFull, etc. // String representation of a property s, _ := mgr.GetStringProperty(int32(battery.BatteryPropertyCapacity)) // Estimated charge time (ms); -1 if not charging chargeTime, _ := mgr.ComputeChargeTimeRemaining() fmt.Printf("Battery: %d%%, charging=%v, est. %d min\n", capacity, charging, chargeTime/60000) // Output: Battery: 85%, charging=true, est. 32 min ``` --- ### bluetooth — BluetoothAdapter ```go import ( "github.com/AndroidGoLab/jni/bluetooth" "github.com/AndroidGoLab/jni/bluetooth/le" ) adapter, err := bluetooth.NewAdapter(ctx) if err != nil { return fmt.Errorf("bluetooth.NewAdapter: %w", err) } defer adapter.Close() // State queries enabled, _ := adapter.IsEnabled() name, _ := adapter.GetName() addr, _ := adapter.GetAddress() scanMode, _ := adapter.GetScanMode() // Scan mode constants: bluetooth.ScanModeNone, ScanModeConnectable, ScanModeConnectableDiscoverable // LE capability checks leSupported, _ := adapter.IsLe2MPhySupported() leAudio, _ := adapter.IsLeAudioSupported() maxAdvLen, _ := adapter.GetLeMaximumAdvertisingDataLength() // Classic discovery started, _ := adapter.StartDiscovery() fmt.Printf("Adapter: %s (%s) enabled=%v scanning=%v\n", name, addr, enabled, started) // Get LE scanner and advertiser (return *jni.Object; wrap into le package types) scannerObj, _ := adapter.GetBluetoothLeScanner() scanner := le.BluetoothLeScanner{VM: vm, Obj: (*jni.GlobalRef)(unsafe.Pointer(scannerObj))} advertiserObj, _ := adapter.GetBluetoothLeAdvertiser() advertiser := le.BluetoothLeAdvertiser{VM: vm, Obj: (*jni.GlobalRef)(unsafe.Pointer(advertiserObj))} _ = scanner _ = advertiser // BLE scanning via scanner // scanner.StartScan(callbackObj) // scanner.StopScan1(callbackObj) ``` --- ### location — LocationManager ```go import ( "github.com/AndroidGoLab/jni/location" goos "github.com/AndroidGoLab/jni/os" ) mgr, err := location.NewManager(ctx) if err != nil { return err } defer mgr.Close() // Check provider availability gpsOK, _ := mgr.IsProviderEnabled(location.GpsProvider) // "gps" netOK, _ := mgr.IsProviderEnabled(location.NetworkProvider) // "network" locationEnabled, _ := mgr.IsLocationEnabled() // Read last cached location locObj, err := mgr.GetLastKnownLocation(location.GpsProvider) if locObj != nil { loc := &location.Location{VM: mgr.VM, Obj: locObj} lat, _ := loc.GetLatitude() lon, _ := loc.GetLongitude() fmt.Printf("Cached: %.6f, %.6f\n", lat, lon) } // Request live updates with a LocationListener proxy (interface → env.NewProxy) // and a HandlerThread for Looper-based callback delivery: ht, _ := goos.NewHandlerThread(vm, "LocationThread") defer ht.Close() looper, _ := ht.GetLooper() vm.Do(func(env *jni.Env) error { lsCls, _ := env.FindClass("android/location/LocationListener") proxy, cleanup, err := env.NewProxy( []*jni.Class{lsCls}, func(env *jni.Env, method string, args []*jni.Object) (*jni.Object, error) { if method == "onLocationChanged" { locGlobal := env.NewGlobalRef(args[0]) loc := &location.Location{VM: vm, Obj: locGlobal} lat, _ := loc.GetLatitude() lon, _ := loc.GetLongitude() fmt.Printf("Live fix: %.6f, %.6f\n", lat, lon) env.DeleteGlobalRef(locGlobal) } return nil, nil }, ) if err != nil { return err } defer cleanup() listenerGlobal := env.NewGlobalRef(proxy) defer env.DeleteLocalRef(proxy) // requestLocationUpdates(String, long, float, Listener, Looper) return mgr.RequestLocationUpdates5_4( location.GpsProvider, 0, 0, listenerGlobal, looper) }) fmt.Printf("GPS=%v, Network=%v, LocationEnabled=%v\n", gpsOK, netOK, locationEnabled) ``` --- ### telephony — TelephonyManager ```go import "github.com/AndroidGoLab/jni/telephony" mgr, err := telephony.NewManager(ctx) if err != nil { return err } defer mgr.Close() phoneType, _ := mgr.GetPhoneType() switch int(phoneType) { case telephony.PhoneTypeGsm: fmt.Println("GSM phone") case telephony.PhoneTypeCdma: fmt.Println("CDMA phone") case telephony.PhoneTypeNone: fmt.Println("No radio") } simState, _ := mgr.GetSimState0() if int(simState) == telephony.SimStateReady { operatorName, _ := mgr.GetNetworkOperatorName() // e.g., "T-Mobile" mccMnc, _ := mgr.GetNetworkOperator() // e.g., "310260" roaming, _ := mgr.IsNetworkRoaming() fmt.Printf("Operator: %s (%s), roaming: %v\n", operatorName, mccMnc, roaming) } dataState, _ := mgr.GetDataState() // telephony.DataConnected, DataDisconnected, DataConnecting, DataSuspended... fmt.Printf("Data state: %d\n", dataState) ``` --- ### app/notification — NotificationManager ```go import "github.com/AndroidGoLab/jni/app/notification" mgr, err := notification.NewManager(ctx) if err != nil { return err } defer mgr.Close() // Create a notification channel (required API 26+) ch, err := notification.NewChannel(vm, "alerts", "Alerts", int32(notification.ImportanceHigh)) if err != nil { return err } mgr.CreateNotificationChannel(ch.Obj) // Get application context for the builder appCtxObj, _ := ctx.GetApplicationContext() // Build the notification b, _ := notification.NewBuilder(vm, appCtxObj, "alerts") b.SetSmallIcon1_1(17301620) // android.R.drawable.ic_dialog_info b.SetContentTitle("Hello from Go!") b.SetContentText("Posted via JNI with no Java code") notif, err := b.Build() if err != nil { return err } // Post (notify) if err := mgr.Notify2(42, notif); err != nil { return fmt.Errorf("notify: %w", err) } // Cancel mgr.Cancel1(42) // Importance level constants: // notification.ImportanceNone (0), ImportanceMin (1), ImportanceLow (2), // notification.ImportanceDefault (3), ImportanceHigh (4) ``` --- ### content/resolver — ContentResolver and Cursor ```go import "github.com/AndroidGoLab/jni/content/resolver" // Get resolver from context resolverObj, _ := ctx.GetContentResolver() cr := resolver.ContentResolver{VM: vm, Obj: resolverObj} // Parse a content URI uriHelper := resolver.Uri{VM: vm} uri, _ := uriHelper.Parse("content://settings/system") // Other common URIs: // "content://com.android.contacts/contacts" (requires READ_CONTACTS) // "content://media/external/images/media" (requires READ_MEDIA_IMAGES) // Execute a query cursorObj, err := cr.Query4(uri, nil, nil, nil) // (uri, projection, selection, selectionArgs) if err != nil || cursorObj == nil { return err } cursor := resolver.Cursor{VM: vm, Obj: cursorObj} defer cursor.Close() rowCount, _ := cursor.GetCount() colCount, _ := cursor.GetColumnCount() fmt.Printf("%d rows × %d columns\n", rowCount, colCount) // Iterate rows for ok, _ := cursor.MoveToFirst(); ok; ok, _ = cursor.MoveToNext() { for c := int32(0); c < colCount; c++ { val, _ := cursor.GetString(c) fmt.Printf("%s\t", val) } fmt.Println() } // Output (example): // 64 rows × 2 columns // bluetooth_on 1 // screen_brightness 128 // ... ``` --- ### os/build — Build Info and API Level ```go import "github.com/AndroidGoLab/jni/os/build" // Read device build info (static fields from android.os.Build) info, err := build.GetBuildInfo(vm) if err != nil { return err } fmt.Printf("Device: %s\n", info.Device) fmt.Printf("Model: %s\n", info.Model) fmt.Printf("Product: %s\n", info.Product) fmt.Printf("Manufacturer: %s\n", info.Manufacturer) fmt.Printf("Brand: %s\n", info.Brand) fmt.Printf("Board: %s\n", info.Board) fmt.Printf("Hardware: %s\n", info.Hardware) // Read Android version info (android.os.Build.VERSION) ver, err := build.GetVersionInfo(vm) if err != nil { return err } fmt.Printf("API level: %d\n", ver.SDKInt) // e.g., 34 fmt.Printf("Release: %s\n", ver.Release) // e.g., "14" fmt.Printf("Codename: %s\n", ver.Codename) // e.g., "REL" fmt.Printf("Incremental: %s\n", ver.Incremental) // e.g., "10964766" // Read device serial (requires READ_PHONE_STATE permission) b := build.Build{VM: vm} serial, err := b.GetSerial() ``` --- ### os — HandlerThread for callback delivery Many Android callback APIs require a `Looper` thread. `os.NewHandlerThread` creates and starts a `HandlerThread` in one call. Pass its `Looper` to APIs like `LocationManager.requestLocationUpdates`. ```go import goos "github.com/AndroidGoLab/jni/os" // Create and start a HandlerThread ht, err := goos.NewHandlerThread(vm, "GoCallbackThread") if err != nil { return fmt.Errorf("new HandlerThread: %w", err) } defer ht.Close() // Quits the thread safely and releases the global ref // Get its Looper for callback delivery registration looperObj, err := ht.GetLooper() if err != nil { return err } // Pass looperObj to any API that needs a Looper: // mgr.RegisterGnssMeasurementsCallback2_2(callbackObj, looperObj) // mgr.RequestLocationUpdates5_4(provider, minTime, minDist, listener, looperObj) // p2pMgr.RequestPeers(channel, listenerObj) // WifiP2pManager fmt.Println("HandlerThread ready, looper:", looperObj) ``` --- ### bluetooth — BluetoothGatt (GATT Client) ```go // Connect to a remote BLE device gattObj, err := device.ConnectGatt3( (*jni.Object)(unsafe.Pointer(ctx.Obj)), // Context false, // autoConnect callbackProxy, // BluetoothGattCallback (abstract — use Java adapter) ) if err != nil { return fmt.Errorf("ConnectGatt: %w", err) } gatt := bluetooth.Gatt{VM: vm, Obj: (*jni.GlobalRef)(unsafe.Pointer(gattObj))} defer gatt.Close() // Discover services (fires onServicesDiscovered callback) ok, _ := gatt.DiscoverServices() // After discovery, get a service and characteristic serviceObj, _ := gatt.GetService(uuidObj) // returns *jni.Object ok, _ = gatt.ReadCharacteristic(charObj) // fires onCharacteristicRead callback ok, _ = gatt.WriteCharacteristic1(charObj) // fires onCharacteristicWrite callback ok, _ = gatt.SetCharacteristicNotification(charObj, true) // Request an MTU size (max BLE ATT MTU = 517) ok, _ = gatt.RequestMtu(int32(517)) // Read remote RSSI (fires onReadRemoteRssi callback) ok, _ = gatt.ReadRemoteRssi() fmt.Printf("GATT services discovered=%v\n", ok) // GATT constants: // bluetooth.GattSuccess (0), GattFailure (257) // bluetooth.StateConnected (2), StateDisconnected (0) // bluetooth.PropertyRead (2), PropertyNotify (16), PropertyWrite (8) ``` --- ### net/wifi — WifiManager ```go import "github.com/AndroidGoLab/jni/net/wifi" mgr, err := wifi.NewManager(ctx) if err != nil { return err } defer mgr.Close() // Check state wifiState, _ := mgr.GetWifiState() // wifi.WifiStateEnabled (3), WifiStateDisabled (1), WifiStateEnabling (2), etc. enabled, _ := mgr.IsWifiEnabled() // Get connection info connInfo, _ := mgr.GetConnectionInfo() // returns *jni.Object (WifiInfo) // Start scan started, _ := mgr.StartScan() // Scan results arrive via ACTION_SCAN_RESULTS_AVAILABLE broadcast // Named constants: // wifi.WifiStateDisabling (0), WifiStateDisabled (1), WifiStateEnabling (2) // wifi.WifiStateEnabled (3), WifiStateUnknown (4) // wifi.WifiStateChangedAction, wifi.ScanResultsAvailableAction // wifi.ExtraWifiState, wifi.ExtraNewState, wifi.ExtraSupplicantConnected fmt.Printf("WiFi: enabled=%v state=%d scan started=%v\n", enabled, wifiState, started) ``` --- ## JNI Type Signatures Reference Used in `GetMethodID`, `GetStaticMethodID`, `GetFieldID`, `GetStaticFieldID`. | Java Type | Signature | Java Type | Signature | |---|---|---|---| | `boolean` | `Z` | `String` | `Ljava/lang/String;` | | `byte` | `B` | `Object` | `Ljava/lang/Object;` | | `char` | `C` | `int[]` | `[I` | | `short` | `S` | `String[]` | `[Ljava/lang/String;` | | `int` | `I` | `void` | `V` | | `long` | `J` | inner class | `Landroid/os/Build$VERSION;` | | `float` | `F` | | | | `double` | `D` | | | Method signatures: `(param-types)return-type`. Examples: - `()Z` — no args, returns boolean - `(Ljava/lang/String;)V` — takes String, returns void - `(Ljava/lang/String;JFLandroid/location/LocationListener;Landroid/os/Looper;)V` — 5-arg requestLocationUpdates --- ## Summary `go-jni` is the comprehensive Android JNI toolkit for Go developers. Its primary use cases span: reading device hardware state (battery level and charging status, Bluetooth adapter name and MAC, location fixes from GPS/network, Android API level from `Build.VERSION`), interacting with Android services (posting system notifications, querying content providers like contacts and settings, managing WiFi and cellular connectivity, driving Bluetooth discovery and BLE scan/advertise/GATT), and implementing callbacks (receiving system broadcasts via `BroadcastReceiver`, implementing `LocationListener` or `WifiP2pManager.ActionListener` entirely in Go using `env.NewProxy`, or bridging abstract-class callbacks like `ScanCallback` through the `GoAbstractDispatch` Java adapter pattern). All 53 Android API packages eliminate raw JNI boilerplate: a single `NewManager(ctx)` call returns a fully initialized Go wrapper whose typed methods handle thread attachment, method-ID caching, exception checking, and local/global reference lifetime. Integration with any Go Android framework follows the same three-step pattern: obtain a `*jni.VM` from the framework entry point (`jni.VMFromUintptr(uintptr(app.JavaVM()))` for Gio, or from `ANativeActivity.vm` for NativeActivity), wrap the Activity/Context object with `app.ContextFromObject(vm, globalRef)`, and then use any of the 53 package managers with `defer mgr.Close()`. Callbacks that require a `Looper` thread are served by `os.NewHandlerThread`; Java interface callbacks are implemented with `env.NewProxy`; abstract class callbacks (BroadcastReceiver, ScanCallback, etc.) use the `RegisterProxyHandler` + Java adapter (`GoAbstractDispatch`) pattern. The `consts` sub-package in each wrapper package provides all Android SDK integer constants as named Go identifiers, preventing magic-number bugs across API-level-conditional code paths.