@@ -18,6 +18,7 @@ import {
1818 BeforeRequestDevice ,
1919 BoardVersion ,
2020 ConnectOptions ,
21+ ConnectionAvailabilityStatus ,
2122 ConnectionStatus ,
2223 ConnectionStatusEvent ,
2324 DeviceConnection ,
@@ -42,6 +43,8 @@ import {
4243 TypedServiceEvent ,
4344} from "./service-events.js" ;
4445
46+ import { throwIfUnavailable } from "./availability.js" ;
47+
4548type BleClientError = { message : string ; errorMessage : string } ;
4649
4750let bleClientInitialized = false ;
@@ -181,7 +184,7 @@ class MicrobitWebBluetoothConnectionImpl
181184 extends TypedEventTarget < DeviceConnectionEventMap & ServiceConnectionEventMap >
182185 implements MicrobitWebBluetoothConnection
183186{
184- status : ConnectionStatus = ConnectionStatus . SUPPORT_NOT_KNOWN ;
187+ status : ConnectionStatus = ConnectionStatus . NO_AUTHORIZED_DEVICE ;
185188
186189 /**
187190 * The USB device we last connected to.
@@ -192,12 +195,6 @@ class MicrobitWebBluetoothConnectionImpl
192195 private logging : Logging ;
193196 private connection : BluetoothDeviceWrapper | undefined ;
194197
195- private availabilityListener = ( e : Event ) => {
196- // TODO: is this called? is `value` correct?
197- const value = ( e as any ) . value as boolean ;
198- this . availability = value ;
199- } ;
200- private availability : boolean | undefined ;
201198 private nameFilter : string | undefined ;
202199 private deferStatusUpdates : boolean = false ;
203200
@@ -222,24 +219,73 @@ class MicrobitWebBluetoothConnectionImpl
222219 this . logging . error ( message , e ) ;
223220 }
224221
225- async initialize ( ) : Promise < void > {
226- navigator . bluetooth ?. addEventListener (
227- "availabilitychanged" ,
228- this . availabilityListener ,
229- ) ;
230- this . availability = await navigator . bluetooth ?. getAvailability ( ) ;
231- this . setStatus (
232- this . availability
233- ? ConnectionStatus . NO_AUTHORIZED_DEVICE
234- : ConnectionStatus . NOT_SUPPORTED ,
235- ) ;
222+ async initialize ( ) : Promise < void > { }
223+
224+ dispose ( ) { }
225+
226+ async checkAvailability ( ) : Promise < ConnectionAvailabilityStatus > {
227+ if ( Capacitor . isNativePlatform ( ) ) {
228+ return this . checkNativeBluetoothAvailability ( ) ;
229+ }
230+ return this . checkWebBluetoothAvailability ( ) ;
236231 }
237232
238- dispose ( ) {
239- navigator . bluetooth ?. removeEventListener (
240- "availabilitychanged" ,
241- this . availabilityListener ,
242- ) ;
233+ private async checkWebBluetoothAvailability ( ) : Promise < ConnectionAvailabilityStatus > {
234+ if ( ! navigator . bluetooth ) {
235+ return "unsupported" ;
236+ }
237+
238+ try {
239+ const available = await navigator . bluetooth . getAvailability ( ) ;
240+ return available ? "available" : "disabled" ;
241+ } catch {
242+ return "disabled" ;
243+ }
244+ }
245+
246+ private async checkNativeBluetoothAvailability ( ) : Promise < ConnectionAvailabilityStatus > {
247+ try {
248+ // On Android, check if location services are enabled. This is only
249+ // required on Android < 12 (API < 31), but isLocationEnabled() returns
250+ // true on newer Android, so we can always check it.
251+ if ( isAndroid ( ) ) {
252+ const isLocationEnabled = await BleClient . isLocationEnabled ( ) ;
253+ if ( ! isLocationEnabled ) {
254+ return "location-disabled" ;
255+ }
256+ }
257+
258+ // Initialize BLE (requests permissions automatically on first call).
259+ if ( ! bleClientInitialized ) {
260+ await BleClient . initialize ( { androidNeverForLocation : true } ) ;
261+ bleClientInitialized = true ;
262+ }
263+
264+ // Check if Bluetooth is enabled.
265+ const isBluetoothEnabled = await BleClient . isEnabled ( ) ;
266+ if ( ! isBluetoothEnabled ) {
267+ return "disabled" ;
268+ }
269+
270+ return "available" ;
271+ } catch ( e ) {
272+ // Handle errors from BleClient.initialize() which rejects for permission
273+ // or unsupported states. Error messages are hardcoded in the plugin:
274+ // https://github.com/capacitor-community/bluetooth-le/blob/main/ios/Plugin/DeviceManager.swift
275+ const errorMessage =
276+ e instanceof Error ? e . message : ( e as BleClientError ) ?. message ?? "" ;
277+ this . log ( `Bluetooth availability check failed: "${ errorMessage } "` ) ;
278+
279+ if ( errorMessage === "BLE permission denied" ) {
280+ return "permission-denied" ;
281+ }
282+ if ( errorMessage === "BLE unsupported" ) {
283+ return "unsupported" ;
284+ }
285+
286+ // Unknown error - default to permission-denied
287+ return "permission-denied" ;
288+ }
243289 }
244290
245291 getBoardVersion ( ) : BoardVersion | undefined {
@@ -249,8 +295,10 @@ class MicrobitWebBluetoothConnectionImpl
249295 async connect ( options ?: ConnectOptions ) : Promise < void > {
250296 const progress = options ?. progress ?? ( ( ) => { } ) ;
251297
298+ // Check availability before connecting. Done here rather than at initialize()
299+ // because on Android/iOS that's the appropriate time to ask for permissions.
252300 progress ( ProgressStage . Initializing ) ;
253- await this . preConnectInitialization ( ) ;
301+ throwIfUnavailable ( await this . checkAvailability ( ) ) ;
254302
255303 if ( ! this . connection ) {
256304 progress ( ProgressStage . FindingDevice ) ;
@@ -276,53 +324,6 @@ class MicrobitWebBluetoothConnectionImpl
276324 await this . connection . connect ( ) ;
277325 }
278326
279- /**
280- * Initializes BLE.
281- *
282- * This must happen before requesting a device or connecting.
283- *
284- * We do this just before use not at connection initialize because on Android/iOS
285- * that's the appropriate time to ask for permissions.
286- */
287- private async preConnectInitialization ( ) : Promise < void > {
288- try {
289- if ( isAndroid ( ) ) {
290- const isLocationEnabled = await BleClient . isLocationEnabled ( ) ;
291- if ( ! isLocationEnabled ) {
292- throw new DeviceError ( {
293- code : "bluetooth-missing-permissions" ,
294- message : "Location services is disabled" ,
295- } ) ;
296- }
297- }
298- if ( ! bleClientInitialized ) {
299- await BleClient . initialize ( { androidNeverForLocation : true } ) ;
300- bleClientInitialized = true ;
301- }
302- const isBluetoothEnabled = await BleClient . isEnabled ( ) ;
303- if ( ! isBluetoothEnabled ) {
304- throw new DeviceError ( {
305- code : "bluetooth-disabled" ,
306- message : "Bluetooth is disabled" ,
307- } ) ;
308- }
309- } catch ( e : unknown ) {
310- this . error ( "Error initializing Bluetooth" , e ) ;
311- const error = e as BleClientError ;
312- if ( error . message === "BLE permission denied" ) {
313- // Error thrown for iOS platform.
314- throw new DeviceError ( {
315- code : "bluetooth-disabled" ,
316- message : "Bluetooth is disabled" ,
317- } ) ;
318- }
319- throw new DeviceError ( {
320- code : "bluetooth-missing-permissions" ,
321- message : "Missing permissions" ,
322- } ) ;
323- }
324- }
325-
326327 async disconnect ( ) : Promise < void > {
327328 try {
328329 if ( this . connection ) {
0 commit comments