Skip to content

Commit a5463b2

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: add NewIntentFilter, NewHandlerThread, GetParcelableExtra
Eliminate raw JNI gaps that forced documentation to use ugly FindClass/ GetMethodID/NewObject patterns instead of typed wrappers: - content.NewIntentFilter(vm, action): creates IntentFilter with typed constructor, supports empty action for multi-action filters - os.NewHandlerThread(vm, name): creates and starts a HandlerThread in one call, with Close() that quits safely and releases the global ref - app.Intent.GetParcelableExtra(key): hand-written method for the deprecated but widely-used getParcelableExtra(String) Update all docs to use the new typed APIs. Raw JNI now only appears for env.NewProxy() interface lookups (inherent to the proxy pattern) and user-defined Java adapter class construction (GoBroadcastReceiver, GoScanCallback).
1 parent 55ddf85 commit a5463b2

7 files changed

Lines changed: 398 additions & 275 deletions

File tree

app/intent_parcelable_extra.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package app
2+
3+
import (
4+
"fmt"
5+
"unsafe"
6+
7+
"github.com/AndroidGoLab/jni"
8+
)
9+
10+
// midIntentGetParcelableExtra is lazily initialized on first call.
11+
var midIntentGetParcelableExtra jni.MethodID
12+
13+
// GetParcelableExtra calls android.content.Intent.getParcelableExtra(String).
14+
// This method is deprecated in API 33 but remains the standard way to extract
15+
// Parcelable extras on older API levels.
16+
func (m *Intent) GetParcelableExtra(
17+
key string,
18+
) (*jni.Object, error) {
19+
var result *jni.Object
20+
var callErr error
21+
callErr = m.VM.Do(func(env *jni.Env) error {
22+
if err := ensureInit(env); err != nil {
23+
return err
24+
}
25+
26+
if midIntentGetParcelableExtra == nil {
27+
mid, err := env.GetMethodID(
28+
(*jni.Class)(unsafe.Pointer(clsIntent)),
29+
"getParcelableExtra",
30+
"(Ljava/lang/String;)Landroid/os/Parcelable;",
31+
)
32+
if err != nil {
33+
callErr = fmt.Errorf("android.content.Intent.getParcelableExtra is not available on this device")
34+
return callErr
35+
}
36+
midIntentGetParcelableExtra = mid
37+
}
38+
39+
jKey, err := env.NewStringUTF(key)
40+
if err != nil {
41+
return err
42+
}
43+
defer env.DeleteLocalRef(&jKey.Object)
44+
45+
result, callErr = env.CallObjectMethod(
46+
m.Obj,
47+
midIntentGetParcelableExtra, jni.ObjectValue(&jKey.Object),
48+
)
49+
if callErr != nil {
50+
return callErr
51+
}
52+
if result != nil {
53+
localRef := result
54+
result = env.NewGlobalRef(localRef)
55+
env.DeleteLocalRef(localRef)
56+
}
57+
return callErr
58+
})
59+
return result, callErr
60+
}

content/new_intent_filter.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package content
2+
3+
import (
4+
"fmt"
5+
"unsafe"
6+
7+
"github.com/AndroidGoLab/jni"
8+
)
9+
10+
// NewIntentFilter creates a new IntentFilter with the given action.
11+
// Pass an empty string to create a filter with no initial action.
12+
func NewIntentFilter(
13+
vm *jni.VM,
14+
action string,
15+
) (*IntentFilter, error) {
16+
if vm == nil {
17+
return nil, fmt.Errorf("content.NewIntentFilter: nil VM")
18+
}
19+
var filter IntentFilter
20+
filter.VM = vm
21+
22+
err := filter.VM.Do(func(env *jni.Env) error {
23+
if err := ensureInit(env); err != nil {
24+
return err
25+
}
26+
27+
cls := (*jni.Class)(unsafe.Pointer(clsIntentFilter))
28+
29+
var obj *jni.Object
30+
switch {
31+
case action != "":
32+
initMid, err := env.GetMethodID(cls, "<init>", "(Ljava/lang/String;)V")
33+
if err != nil {
34+
return fmt.Errorf("get IntentFilter(String) constructor: %w", err)
35+
}
36+
jAction, err := env.NewStringUTF(action)
37+
if err != nil {
38+
return err
39+
}
40+
defer env.DeleteLocalRef(&jAction.Object)
41+
obj, err = env.NewObject(cls, initMid, jni.ObjectValue(&jAction.Object))
42+
if err != nil {
43+
return fmt.Errorf("new IntentFilter(%q): %w", action, err)
44+
}
45+
default:
46+
initMid, err := env.GetMethodID(cls, "<init>", "()V")
47+
if err != nil {
48+
return fmt.Errorf("get IntentFilter() constructor: %w", err)
49+
}
50+
obj, err = env.NewObject(cls, initMid)
51+
if err != nil {
52+
return fmt.Errorf("new IntentFilter(): %w", err)
53+
}
54+
}
55+
56+
filter.Obj = env.NewGlobalRef(obj)
57+
env.DeleteLocalRef(obj)
58+
return nil
59+
})
60+
if err != nil {
61+
return nil, err
62+
}
63+
return &filter, nil
64+
}
65+
66+
// Close releases the global reference to the underlying Java object.
67+
func (m *IntentFilter) Close() {
68+
if m.Obj != nil {
69+
_ = m.VM.Do(func(env *jni.Env) error {
70+
env.DeleteGlobalRef(m.Obj)
71+
m.Obj = nil
72+
return nil
73+
})
74+
}
75+
}

docs/bluetooth.md

Lines changed: 57 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ func queryAdapter(ctx *app.Context) error {
2424
defer adapter.Close() // releases the JNI global reference
2525

2626
// Check if Bluetooth hardware is enabled.
27-
// Internally: env.CallBooleanMethod(obj, mid) -> uint8, then resultRaw != 0
2827
enabled, err := adapter.IsEnabled()
2928
if err != nil {
3029
return err
3130
}
3231
fmt.Printf("Bluetooth enabled: %v\n", enabled)
3332

34-
// Adapter name and MAC address (both return string via CallObjectMethod + GoString)
33+
// Adapter name and MAC address (both return string)
3534
name, err := adapter.GetName()
3635
if err != nil {
3736
return err
@@ -42,12 +41,19 @@ func queryAdapter(ctx *app.Context) error {
4241
}
4342
fmt.Printf("Adapter: %s (%s)\n", name, addr)
4443

45-
// Scan mode (int32)
44+
// Scan mode (int32) -- compare against named constants
4645
scanMode, err := adapter.GetScanMode()
4746
if err != nil {
4847
return err
4948
}
50-
fmt.Printf("Scan mode: %d\n", scanMode)
49+
switch scanMode {
50+
case bluetooth.ScanModeNone:
51+
fmt.Println("Scan mode: none")
52+
case bluetooth.ScanModeConnectable:
53+
fmt.Println("Scan mode: connectable")
54+
case bluetooth.ScanModeConnectableDiscoverable:
55+
fmt.Println("Scan mode: connectable + discoverable")
56+
}
5157

5258
return nil
5359
}
@@ -110,6 +116,7 @@ import (
110116
"github.com/AndroidGoLab/jni"
111117
"github.com/AndroidGoLab/jni/app"
112118
"github.com/AndroidGoLab/jni/bluetooth"
119+
"github.com/AndroidGoLab/jni/content"
113120
)
114121

115122
func runDiscovery(vm *jni.VM, ctx *app.Context) error {
@@ -136,57 +143,54 @@ func runDiscovery(vm *jni.VM, ctx *app.Context) error {
136143
if methodName != "onReceive" || len(args) < 2 {
137144
return nil, nil
138145
}
139-
// args[0] = Context, args[1] = Intent
140-
intentObj := args[1]
146+
// args[0] = Context, args[1] = Intent (local ref)
141147

142-
// Extract the BluetoothDevice from the intent via raw JNI:
143-
// BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
144-
intentClass, err := env.FindClass("android/content/Intent")
145-
if err != nil {
146-
return nil, err
147-
}
148-
defer env.DeleteLocalRef(&intentClass.Object)
148+
// Wrap the intent in a typed wrapper. Promote to global ref first
149+
// because Intent methods call vm.Do() internally.
150+
intentGlobal := env.NewGlobalRef(args[1])
151+
intent := app.Intent{VM: vm, Obj: intentGlobal}
152+
defer env.DeleteGlobalRef(intentGlobal)
149153

150-
getParcelableMid, err := env.GetMethodID(intentClass,
151-
"getParcelableExtra",
152-
"(Ljava/lang/String;)Landroid/os/Parcelable;")
154+
// Use typed wrapper to read the action.
155+
action, err := intent.GetAction()
153156
if err != nil {
154157
return nil, err
155158
}
156-
157-
extraKey, err := env.NewStringUTF("android.bluetooth.device.extra.DEVICE")
158-
if err != nil {
159-
return nil, err
159+
if action != bluetooth.ActionFound {
160+
return nil, nil
160161
}
161-
defer env.DeleteLocalRef(&extraKey.Object)
162162

163-
deviceObj, err := env.CallObjectMethod(intentObj, getParcelableMid,
164-
jni.ObjectValue(&extraKey.Object))
163+
// Extract the BluetoothDevice from the intent extras.
164+
deviceObj, err := intent.GetParcelableExtra(bluetooth.ExtraDevice)
165165
if err != nil || deviceObj == nil {
166166
return nil, err
167167
}
168-
defer env.DeleteLocalRef(deviceObj)
169168

170169
// Wrap in the generated Device type to use typed accessors.
171-
// We need a global ref for the Device struct.
172170
deviceGlobal := env.NewGlobalRef(deviceObj)
173171
device := bluetooth.Device{VM: vm, Obj: deviceGlobal}
172+
defer env.DeleteGlobalRef(deviceGlobal)
174173

175174
name, _ := device.GetName()
176175
addr, _ := device.GetAddress()
177176
devType, _ := device.GetType()
178177
fmt.Printf("Found device: %s (%s) type=%d\n", name, addr, devType)
179178

180-
env.DeleteGlobalRef(deviceGlobal)
181179
return nil, nil
182180
},
183181
)
184182
defer jni.UnregisterProxyHandler(handlerID)
185183

186-
// 2. Instantiate GoBroadcastReceiver and IntentFilter via raw JNI.
184+
// 2. Create an IntentFilter for ACTION_FOUND.
185+
filter, err := content.NewIntentFilter(vm, bluetooth.ActionFound)
186+
if err != nil {
187+
return fmt.Errorf("new IntentFilter: %w", err)
188+
}
189+
defer filter.Close()
190+
191+
// 3. Instantiate GoBroadcastReceiver (user-defined Java adapter -- raw JNI required).
187192
var receiverGlobal *jni.GlobalRef
188193
err = vm.Do(func(env *jni.Env) error {
189-
// Create the GoBroadcastReceiver(handlerID)
190194
recvClass, err := env.FindClass("center/dx/jni/generated/GoBroadcastReceiver")
191195
if err != nil {
192196
return fmt.Errorf("find GoBroadcastReceiver: %w", err)
@@ -203,46 +207,22 @@ func runDiscovery(vm *jni.VM, ctx *app.Context) error {
203207
}
204208
receiverGlobal = env.NewGlobalRef(recvLocal)
205209
env.DeleteLocalRef(recvLocal)
206-
207-
// Create IntentFilter("android.bluetooth.device.action.FOUND")
208-
ifClass, err := env.FindClass("android/content/IntentFilter")
209-
if err != nil {
210-
return err
211-
}
212-
defer env.DeleteLocalRef(&ifClass.Object)
213-
214-
ifInit, err := env.GetMethodID(ifClass, "<init>", "(Ljava/lang/String;)V")
215-
if err != nil {
216-
return err
217-
}
218-
actionStr, err := env.NewStringUTF("android.bluetooth.device.action.FOUND")
219-
if err != nil {
220-
return err
221-
}
222-
defer env.DeleteLocalRef(&actionStr.Object)
223-
224-
filterLocal, err := env.NewObject(ifClass, ifInit, jni.ObjectValue(&actionStr.Object))
225-
if err != nil {
226-
return err
227-
}
228-
filterGlobal := env.NewGlobalRef(filterLocal)
229-
env.DeleteLocalRef(filterLocal)
230-
defer func() { env.DeleteGlobalRef(filterGlobal) }()
231-
232-
// Register the receiver with the Context.
233-
// ctx.RegisterReceiver2(receiver, filter) -> Intent
234-
_, err = ctx.RegisterReceiver2(
235-
(*jni.Object)(unsafe.Pointer(receiverGlobal)),
236-
(*jni.Object)(unsafe.Pointer(filterGlobal)),
237-
)
238-
return err
210+
return nil
239211
})
212+
if err != nil {
213+
return fmt.Errorf("create receiver: %w", err)
214+
}
215+
216+
// 4. Register the receiver with the Context.
217+
_, err = ctx.RegisterReceiver2(
218+
(*jni.Object)(unsafe.Pointer(receiverGlobal)),
219+
filter.Obj,
220+
)
240221
if err != nil {
241222
return fmt.Errorf("register receiver: %w", err)
242223
}
243224

244-
// 3. Start discovery.
245-
// Internally calls env.CallBooleanMethod with JNI signature "()Z".
225+
// 5. Start discovery.
246226
// Returns (bool, error): true if discovery started successfully.
247227
started, err := adapter.StartDiscovery()
248228
if err != nil {
@@ -255,7 +235,7 @@ func runDiscovery(vm *jni.VM, ctx *app.Context) error {
255235

256236
// ... wait for results, e.g. time.Sleep or channel ...
257237

258-
// 4. Cleanup: cancel discovery + unregister receiver.
238+
// 6. Cleanup: cancel discovery + unregister receiver.
259239
_, _ = adapter.CancelDiscovery()
260240
_ = ctx.UnregisterReceiver((*jni.Object)(unsafe.Pointer(receiverGlobal)))
261241
_ = vm.Do(func(env *jni.Env) error {
@@ -304,10 +284,10 @@ type Device struct {
304284
}
305285

306286
// All accessors follow the same vm.Do + ensureInit + CallXxxMethod pattern.
307-
name, err := device.GetName() // string (CallObjectMethod + GoString)
287+
name, err := device.GetName() // string
308288
addr, err := device.GetAddress() // string
309-
devType, err := device.GetType() // int32 (CallIntMethod)
310-
bondState, err := device.GetBondState() // int32 (CallIntMethod)
289+
devType, err := device.GetType() // int32 -- compare with bluetooth.DeviceTypeClassic, etc.
290+
bondState, err := device.GetBondState() // int32 -- compare with bluetooth.BondNone, etc.
311291
alias, err := device.GetAlias() // string
312292
uuids, err := device.GetUuids() // *jni.Object (raw ParcelUuid[] array)
313293

@@ -417,7 +397,7 @@ public class GoScanCallback extends ScanCallback {
417397
)
418398
defer jni.UnregisterProxyHandler(scanHandlerID)
419399

420-
// Instantiate GoScanCallback via raw JNI.
400+
// raw JNI: GoScanCallback is a user-defined Java class with no generated constructor
421401
var callbackObj *jni.Object
422402
err = vm.Do(func(env *jni.Env) error {
423403
cbClass, err := env.FindClass("center/dx/jni/generated/GoScanCallback")
@@ -647,6 +627,15 @@ bluetooth.StateDisconnecting // 3
647627
bluetooth.ConnectionPriorityBalanced // 0
648628
bluetooth.ConnectionPriorityHigh // 1
649629
bluetooth.ConnectionPriorityLowPower // 2
630+
631+
// Action strings (from BluetoothDevice / BluetoothAdapter)
632+
bluetooth.ActionFound // "android.bluetooth.device.action.FOUND"
633+
bluetooth.ActionBondStateChanged // "android.bluetooth.device.action.BOND_STATE_CHANGED"
634+
bluetooth.ActionDiscoveryStarted // "android.bluetooth.adapter.action.DISCOVERY_STARTED"
635+
bluetooth.ActionDiscoveryFinished // "android.bluetooth.adapter.action.DISCOVERY_FINISHED"
636+
637+
// Extra keys (from BluetoothDevice)
638+
bluetooth.ExtraDevice // "android.bluetooth.device.extra.DEVICE"
650639
```
651640

652641
BLE-specific constants live in `bluetooth/le`:

0 commit comments

Comments
 (0)