Java tutorial
/* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.commontime.plugin; import java.security.InvalidKeyException; import java.util.Collection; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.altbeacon.beacon.Beacon; import org.altbeacon.beacon.BeaconConsumer; import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.BeaconParser; import org.altbeacon.beacon.BleNotAvailableException; import org.altbeacon.beacon.Identifier; import org.altbeacon.beacon.MonitorNotifier; import org.altbeacon.beacon.RangeNotifier; import org.altbeacon.beacon.Region; import org.apache.cordova.CordovaWebView; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaInterface; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.Manifest; import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.RemoteException; import android.util.Log; @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public class LocationManager extends CordovaPlugin implements BeaconConsumer { public static final String TAG = "com.commontime.cordova.beacon"; private static int CDV_LOCATION_MANAGER_DOM_DELEGATE_TIMEOUT = 30; private BeaconManager iBeaconManager; private BlockingQueue<Runnable> queue; private PausableThreadPoolExecutor threadPoolExecutor; private boolean debugEnabled = true; private IBeaconServiceNotifier beaconServiceNotifier; //private static final int PERMISSION_REQUEST_COARSE_LOCATION = 1; //listener for changes in state for system Bluetooth service private BroadcastReceiver broadcastReceiver; private BluetoothAdapter bluetoothAdapter; /** * Constructor. */ public LocationManager() { } /** * Sets the context of the Command. This can then be used to do things like * get file paths associated with the Activity. * * @param cordova The context of the main Activity. * @param webView The CordovaWebView Cordova is running in. */ public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); initBluetoothListener(); initEventQueue(); pauseEventPropagationToDom(); // Before the DOM is loaded we'll just keep collecting the events and fire them later. initLocationManager(); debugEnabled = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { initBluetoothAdapter(); } //TODO AddObserver when page loaded //TODO Due to the below code only compiling when targeting Marshmallow it makes the plugin inflexible when using it below Marshmallow. This is probably better putting this away from the plugin on app start. /* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final Activity act = cordova.getActivity(); // Android M Permission check if (act.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { final AlertDialog.Builder builder = new AlertDialog.Builder(act); builder.setTitle("This app needs location access"); builder.setMessage("Please grant location access so this app can detect beacons."); builder.setPositiveButton(android.R.string.ok, null); builder.setOnDismissListener(new DialogInterface.OnDismissListener() { @SuppressLint("NewApi") @Override public void onDismiss(DialogInterface dialog) { act.requestPermissions(new String[] { Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION); } }); builder.show(); } }*/ } /** * The final call you receive before your activity is destroyed. */ @Override public void onDestroy() { iBeaconManager.unbind(this); if (broadcastReceiver != null) { cordova.getActivity().unregisterReceiver(broadcastReceiver); broadcastReceiver = null; } super.onDestroy(); } //////////////// PLUGIN ENTRY POINT ///////////////////////////// /** * Executes the request and returns PluginResult. * * @param action The action to execute. * @param args JSONArray of arguments for the plugin. * @param callbackContext The callback id used when calling back into JavaScript. * @return True if the action was valid, false if not. */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) { if (action.equals("onDomDelegateReady")) { onDomDelegateReady(callbackContext); } else if (action.equals("disableDebugNotifications")) { disableDebugNotifications(callbackContext); } else if (action.equals("enableDebugNotifications")) { enableDebugNotifications(callbackContext); } else if (action.equals("disableDebugLogs")) { disableDebugLogs(callbackContext); } else if (action.equals("enableDebugLogs")) { enableDebugLogs(callbackContext); } else if (action.equals("appendToDeviceLog")) { appendToDeviceLog(args.optString(0), callbackContext); } else if (action.equals("startMonitoringForRegion")) { startMonitoringForRegion(args.optJSONObject(0), callbackContext); } else if (action.equals("stopMonitoringForRegion")) { stopMonitoringForRegion(args.optJSONObject(0), callbackContext); } else if (action.equals("startRangingBeaconsInRegion")) { startRangingBeaconsInRegion(args.optJSONObject(0), callbackContext); } else if (action.equals("stopRangingBeaconsInRegion")) { stopRangingBeaconsInRegion(args.optJSONObject(0), callbackContext); } else if (action.equals("isRangingAvailable")) { isRangingAvailable(callbackContext); } else if (action.equals("getAuthorizationStatus")) { getAuthorizationStatus(callbackContext); } else if (action.equals("requestWhenInUseAuthorization")) { requestWhenInUseAuthorization(callbackContext); } else if (action.equals("requestAlwaysAuthorization")) { requestAlwaysAuthorization(callbackContext); } else if (action.equals("getMonitoredRegions")) { getMonitoredRegions(callbackContext); } else if (action.equals("getRangedRegions")) { getRangedRegions(callbackContext); } else if (action.equals("requestStateForRegion")) { requestStateForRegion(args.optJSONObject(0), callbackContext); } else if (action.equals("registerDelegateCallbackId")) { registerDelegateCallbackId(args.optJSONObject(0), callbackContext); } else if (action.equals("isMonitoringAvailableForClass")) { isMonitoringAvailableForClass(args.optJSONObject(0), callbackContext); } else if (action.equals("isAdvertisingAvailable")) { isAdvertisingAvailable(callbackContext); } else if (action.equals("isAdvertising")) { isAdvertising(callbackContext); } else if (action.equals("startAdvertising")) { startAdvertising(args.optJSONObject(0), callbackContext); } else if (action.equals("stopAdvertising")) { stopAdvertising(callbackContext); } else if (action.equals("isBluetoothEnabled")) { isBluetoothEnabled(callbackContext); } else if (action.equals("enableBluetooth")) { enableBluetooth(callbackContext); } else if (action.equals("disableBluetooth")) { disableBluetooth(callbackContext); } else { return false; } return true; } ///////////////// SETUP AND VALIDATION ///////////////////////////////// private void initLocationManager() { iBeaconManager = BeaconManager.getInstanceForApplication(cordova.getActivity()); iBeaconManager.getBeaconParsers() .add(new BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24")); iBeaconManager.bind(this); } private void initBluetoothAdapter() { Activity activity = cordova.getActivity(); BluetoothManager bluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter(); } private void pauseEventPropagationToDom() { checkEventQueue(); threadPoolExecutor.pause(); } private void resumeEventPropagationToDom() { checkEventQueue(); threadPoolExecutor.resume(); } private void initBluetoothListener() { //check access if (!hasBlueToothPermission()) { debugWarn("Cannot listen to Bluetooth service when BLUETOOTH permission is not added"); return; } //check device support try { iBeaconManager.checkAvailability(); } catch (Exception e) { //if device does not support iBeacons an error is thrown debugWarn("Cannot listen to Bluetooth service: " + e.getMessage()); return; } if (broadcastReceiver != null) { debugWarn("Already listening to Bluetooth service, not adding again"); return; } broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); // Only listen for Bluetooth server changes if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); final int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.ERROR); debugLog("Bluetooth Service state changed from " + getStateDescription(oldState) + " to " + getStateDescription(state)); switch (state) { case BluetoothAdapter.ERROR: beaconServiceNotifier.didChangeAuthorizationStatus("AuthorizationStatusNotDetermined"); break; case BluetoothAdapter.STATE_OFF: case BluetoothAdapter.STATE_TURNING_OFF: if (oldState == BluetoothAdapter.STATE_ON) beaconServiceNotifier.didChangeAuthorizationStatus("AuthorizationStatusDenied"); break; case BluetoothAdapter.STATE_ON: beaconServiceNotifier.didChangeAuthorizationStatus("AuthorizationStatusAuthorized"); break; case BluetoothAdapter.STATE_TURNING_ON: break; } } } private String getStateDescription(int state) { switch (state) { case BluetoothAdapter.ERROR: return "ERROR"; case BluetoothAdapter.STATE_OFF: return "STATE_OFF"; case BluetoothAdapter.STATE_TURNING_OFF: return "STATE_TURNING_OFF"; case BluetoothAdapter.STATE_ON: return "STATE_ON"; case BluetoothAdapter.STATE_TURNING_ON: return "STATE_TURNING_ON"; } return "ERROR" + state; } }; // Register for broadcasts on BluetoothAdapter state change IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); cordova.getActivity().registerReceiver(broadcastReceiver, filter); } private void initEventQueue() { //queue is limited to one thread at a time queue = new LinkedBlockingQueue<Runnable>(); threadPoolExecutor = new PausableThreadPoolExecutor(queue); //Add a timeout check new Handler().postDelayed(new Runnable() { @Override public void run() { checkIfDomSignaldDelegateReady(); } }, CDV_LOCATION_MANAGER_DOM_DELEGATE_TIMEOUT * 1000); } private void checkEventQueue() { if (threadPoolExecutor != null && queue != null) return; debugWarn("WARNING event queue should not be null."); queue = new LinkedBlockingQueue<Runnable>(); threadPoolExecutor = new PausableThreadPoolExecutor(queue); } private void checkIfDomSignaldDelegateReady() { if (threadPoolExecutor != null && !threadPoolExecutor.isPaused()) return; String warning = "WARNING did not receive delegate ready callback from DOM after " + CDV_LOCATION_MANAGER_DOM_DELEGATE_TIMEOUT + " seconds!"; debugWarn(warning); webView.sendJavascript("console.warn('" + warning + "')"); } ///////// CALLBACKS //////////////////////////// private void createMonitorCallbacks(final CallbackContext callbackContext) { //Monitor callbacks iBeaconManager.setMonitorNotifier(new MonitorNotifier() { @Override public void didEnterRegion(Region region) { debugLog("didEnterRegion INSIDE for " + region.getUniqueId()); dispatchMonitorState("didEnterRegion", MonitorNotifier.INSIDE, region, callbackContext); } @Override public void didExitRegion(Region region) { debugLog("didExitRegion OUTSIDE for " + region.getUniqueId()); dispatchMonitorState("didExitRegion", MonitorNotifier.OUTSIDE, region, callbackContext); } @Override public void didDetermineStateForRegion(int state, Region region) { debugLog("didDetermineStateForRegion '" + nameOfRegionState(state) + "' for region: " + region.getUniqueId()); dispatchMonitorState("didDetermineStateForRegion", state, region, callbackContext); } // Send state to JS callback until told to stop private void dispatchMonitorState(final String eventType, final int state, final Region region, final CallbackContext callbackContext) { threadPoolExecutor.execute(new Runnable() { public void run() { try { JSONObject data = new JSONObject(); data.put("eventType", eventType); data.put("region", mapOfRegion(region)); if (eventType.equals("didDetermineStateForRegion")) { String stateName = nameOfRegionState(state); data.put("state", stateName); } //send and keep reference to callback PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { Log.e(TAG, "'monitoringDidFailForRegion' exception " + e.getCause()); beaconServiceNotifier.monitoringDidFailForRegion(region, e); } } }); } }); } private void createRangingCallbacks(final CallbackContext callbackContext) { iBeaconManager.setRangeNotifier(new RangeNotifier() { @Override public void didRangeBeaconsInRegion(final Collection<Beacon> iBeacons, final Region region) { threadPoolExecutor.execute(new Runnable() { public void run() { try { JSONObject data = new JSONObject(); JSONArray beaconData = new JSONArray(); for (Beacon beacon : iBeacons) { beaconData.put(mapOfBeacon(beacon)); } data.put("eventType", "didRangeBeaconsInRegion"); data.put("region", mapOfRegion(region)); data.put("beacons", beaconData); debugLog("didRangeBeacons: " + data.toString()); //send and keep reference to callback PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { Log.e(TAG, "'rangingBeaconsDidFailForRegion' exception " + e.getCause()); beaconServiceNotifier.rangingBeaconsDidFailForRegion(region, e); } } }); } }); } private void createManagerCallbacks(final CallbackContext callbackContext) { beaconServiceNotifier = new IBeaconServiceNotifier() { @Override public void rangingBeaconsDidFailForRegion(final Region region, final Exception exception) { threadPoolExecutor.execute(new Runnable() { public void run() { sendFailEvent("rangingBeaconsDidFailForRegion", region, exception, callbackContext); } }); } @Override public void monitoringDidFailForRegion(final Region region, final Exception exception) { threadPoolExecutor.execute(new Runnable() { public void run() { sendFailEvent("monitoringDidFailForRegionWithError", region, exception, callbackContext); } }); } @Override public void didStartMonitoringForRegion(final Region region) { threadPoolExecutor.execute(new Runnable() { public void run() { try { JSONObject data = new JSONObject(); data.put("eventType", "didStartMonitoringForRegion"); data.put("region", mapOfRegion(region)); debugLog("didStartMonitoringForRegion: " + data.toString()); //send and keep reference to callback PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { Log.e(TAG, "'startMonitoringForRegion' exception " + e.getCause()); monitoringDidFailForRegion(region, e); } } }); } @Override public void didChangeAuthorizationStatus(final String status) { threadPoolExecutor.execute(new Runnable() { public void run() { try { JSONObject data = new JSONObject(); data.put("eventType", "didChangeAuthorizationStatus"); data.put("authorizationStatus", status); debugLog("didChangeAuthorizationStatus: " + data.toString()); //send and keep reference to callback PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { callbackContext.error("didChangeAuthorizationStatus error: " + e.getMessage()); } } }); } private void sendFailEvent(String eventType, Region region, Exception exception, final CallbackContext callbackContext) { try { JSONObject data = new JSONObject(); data.put("eventType", eventType);//not perfect mapping, but it's very unlikely to happen here data.put("region", mapOfRegion(region)); data.put("error", exception.getMessage()); PluginResult result = new PluginResult(PluginResult.Status.OK, data); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e) { //still failing, so kill all further event dispatch Log.e(TAG, eventType + " error " + e.getMessage()); callbackContext.error(eventType + " error " + e.getMessage()); } } }; } //-------------------------------------------------------------------------- // PLUGIN METHODS //-------------------------------------------------------------------------- /* * onDomDelegateReady: * * Discussion: * Called from the DOM by the LocationManager Javascript object when it's delegate has been set. * This is to notify the native layer that it can start sending queued up events, like didEnterRegion, * didDetermineState, etc. * * Without this mechanism, the messages would get lost in background mode, because the native layer * has no way of knowing when the consumer Javascript code will actually set it's delegate on the * LocationManager of the DOM. */ private void onDomDelegateReady(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { resumeEventPropagationToDom(); return new PluginResult(PluginResult.Status.OK); } }); } private void isBluetoothEnabled(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { //Check the Bluetooth service is running boolean available = bluetoothAdapter != null && bluetoothAdapter.isEnabled(); return new PluginResult(PluginResult.Status.OK, available); } catch (Exception e) { debugWarn("'isBluetoothEnabled' exception " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void enableBluetooth(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { bluetoothAdapter.enable(); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } catch (Exception e) { Log.e(TAG, "'enableBluetooth' service error: " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void disableBluetooth(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { bluetoothAdapter.disable(); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } catch (Exception e) { Log.e(TAG, "'disableBluetooth' service error: " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void disableDebugNotifications(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { debugEnabled = false; BeaconManager.setDebug(false); //android.bluetooth.BluetoothAdapter.DBG = false; return new PluginResult(PluginResult.Status.OK); } }); } private void enableDebugNotifications(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { debugEnabled = true; //android.bluetooth.BluetoothAdapter.DBG = true; BeaconManager.setDebug(true); return new PluginResult(PluginResult.Status.OK); } }); } private void disableDebugLogs(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { debugEnabled = false; BeaconManager.setDebug(false); //android.bluetooth.BluetoothAdapter.DBG = false; return new PluginResult(PluginResult.Status.OK); } }); } private void enableDebugLogs(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { debugEnabled = true; //android.bluetooth.BluetoothAdapter.DBG = true; BeaconManager.setDebug(true); return new PluginResult(PluginResult.Status.OK); } }); } private void appendToDeviceLog(final String message, CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { if (message != null && !message.isEmpty()) { debugLog("[DOM] " + message); return new PluginResult(PluginResult.Status.OK, message); } else { return new PluginResult(PluginResult.Status.ERROR, "Log message not provided"); } } }); } private void startMonitoringForRegion(final JSONObject arguments, final CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { Region region = null; try { region = parseRegion(arguments); iBeaconManager.startMonitoringBeaconsInRegion(region); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); beaconServiceNotifier.didStartMonitoringForRegion(region); return result; } catch (RemoteException e) { Log.e(TAG, "'startMonitoringForRegion' service error: " + e.getCause()); beaconServiceNotifier.monitoringDidFailForRegion(region, e); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { Log.e(TAG, "'startMonitoringForRegion' exception " + e.getCause()); beaconServiceNotifier.monitoringDidFailForRegion(region, e); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void stopMonitoringForRegion(final JSONObject arguments, final CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { Region region = parseRegion(arguments); iBeaconManager.stopMonitoringBeaconsInRegion(region); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } catch (RemoteException e) { Log.e(TAG, "'stopMonitoringForRegion' service error: " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { Log.e(TAG, "'stopMonitoringForRegion' exception " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void startRangingBeaconsInRegion(final JSONObject arguments, final CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { Region region = parseRegion(arguments); iBeaconManager.startRangingBeaconsInRegion(region); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } catch (RemoteException e) { Log.e(TAG, "'startRangingBeaconsInRegion' service error: " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { Log.e(TAG, "'startRangingBeaconsInRegion' exception " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void stopRangingBeaconsInRegion(final JSONObject arguments, CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { Region region = parseRegion(arguments); iBeaconManager.stopRangingBeaconsInRegion(region); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } catch (RemoteException e) { Log.e(TAG, "'stopRangingBeaconsInRegion' service error: " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { Log.e(TAG, "'stopRangingBeaconsInRegion' exception " + e.getCause()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void getAuthorizationStatus(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { //Check app has the necessary permissions if (!hasBlueToothPermission()) { return new PluginResult(PluginResult.Status.ERROR, "Application does not BLUETOOTH or BLUETOOTH_ADMIN permissions"); } //Check the Bluetooth service is running String authStatus = iBeaconManager.checkAvailability() ? "AuthorizationStatusAuthorized" : "AuthorizationStatusDenied"; JSONObject result = new JSONObject(); result.put("authorizationStatus", authStatus); return new PluginResult(PluginResult.Status.OK, result); } catch (BleNotAvailableException e) { //if device does not support iBeacons and error is thrown debugLog("'getAuthorizationStatus' Device not supported: " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { debugWarn("'getAuthorizationStatus' exception " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void requestWhenInUseAuthorization(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { return new PluginResult(PluginResult.Status.OK); } }); } private void requestAlwaysAuthorization(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { return new PluginResult(PluginResult.Status.OK); } }); } private void getMonitoredRegions(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { Collection<Region> regions = iBeaconManager.getMonitoredRegions(); JSONArray regionArray = new JSONArray(); for (Region region : regions) { regionArray.put(mapOfRegion(region)); } return new PluginResult(PluginResult.Status.OK, regionArray); } catch (JSONException e) { debugWarn("'getMonitoredRegions' exception: " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void getRangedRegions(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { Collection<Region> regions = iBeaconManager.getRangedRegions(); JSONArray regionArray = new JSONArray(); for (Region region : regions) { regionArray.put(mapOfRegion(region)); } return new PluginResult(PluginResult.Status.OK, regionArray); } catch (JSONException e) { debugWarn("'getRangedRegions' exception: " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } //NOT IMPLEMENTED: Manually request monitoring scan for region. //This might not even be needed for Android as it should happen no matter what private void requestStateForRegion(final JSONObject arguments, CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { //not supported on Android PluginResult result = new PluginResult(PluginResult.Status.ERROR, "Manual request for monitoring update is not supported on Android"); result.setKeepCallback(true); return result; } }); } private void isRangingAvailable(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { try { //Check the Bluetooth service is running boolean available = iBeaconManager.checkAvailability(); return new PluginResult(PluginResult.Status.OK, available); } catch (BleNotAvailableException e) { //if device does not support iBeacons and error is thrown debugLog("'isRangingAvailable' Device not supported: " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } catch (Exception e) { debugWarn("'isRangingAvailable' exception " + e.getMessage()); return new PluginResult(PluginResult.Status.ERROR, e.getMessage()); } } }); } private void registerDelegateCallbackId(JSONObject arguments, final CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { debugLog("Registering delegate callback ID: " + callbackContext.getCallbackId()); //delegateCallbackId = callbackContext.getCallbackId(); createMonitorCallbacks(callbackContext); createRangingCallbacks(callbackContext); createManagerCallbacks(callbackContext); PluginResult result = new PluginResult(PluginResult.Status.OK); result.setKeepCallback(true); return result; } }); } /* * Checks if the region is supported, both for type and content */ private void isMonitoringAvailableForClass(final JSONObject arguments, final CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { boolean isValid = true; try { parseRegion(arguments); } catch (Exception e) { //will fail is the region is circular or some expected structure is missing isValid = false; } PluginResult result = new PluginResult(PluginResult.Status.OK, isValid); result.setKeepCallback(true); return result; } }); } private void isAdvertisingAvailable(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { //not supported at Android yet (see Android L) PluginResult result = new PluginResult(PluginResult.Status.OK, false); result.setKeepCallback(true); return result; } }); } private void isAdvertising(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { //not supported on Android PluginResult result = new PluginResult(PluginResult.Status.OK, false); result.setKeepCallback(true); return result; } }); } private void startAdvertising(JSONObject arguments, CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { //not supported on Android PluginResult result = new PluginResult(PluginResult.Status.ERROR, "iBeacon Advertising is not supported on Android"); result.setKeepCallback(true); return result; } }); } private void stopAdvertising(CallbackContext callbackContext) { _handleCallSafely(callbackContext, new ILocationManagerCommand() { @Override public PluginResult run() { //not supported on Android PluginResult result = new PluginResult(PluginResult.Status.ERROR, "iBeacon Advertising is not supported on Android"); result.setKeepCallback(true); return result; } }); } /////////// SERIALISATION ///////////////////// private Region parseRegion(JSONObject json) throws JSONException, InvalidKeyException, UnsupportedOperationException { if (!json.has("typeName")) throw new InvalidKeyException("'typeName' is missing, cannot parse Region."); if (!json.has("identifier")) throw new InvalidKeyException("'identifier' is missing, cannot parse Region."); String typeName = json.getString("typeName"); if (typeName.equals("BeaconRegion")) { return parseBeaconRegion(json); } else if (typeName.equals("CircularRegion")) { return parseCircularRegion(json); } else { throw new UnsupportedOperationException("Unsupported region type"); } } /* NOT SUPPORTED, a possible enhancement later */ private Region parseCircularRegion(JSONObject json) throws JSONException, InvalidKeyException, UnsupportedOperationException { if (!json.has("latitude")) throw new InvalidKeyException("'latitude' is missing, cannot parse CircularRegion."); if (!json.has("longitude")) throw new InvalidKeyException("'longitude' is missing, cannot parse CircularRegion."); if (!json.has("radius")) throw new InvalidKeyException("'radius' is missing, cannot parse CircularRegion."); /*String identifier = json.getString("identifier"); double latitude = json.getDouble("latitude"); double longitude = json.getDouble("longitude"); double radius = json.getDouble("radius"); */ throw new UnsupportedOperationException("Circular regions are not supported at present"); } private Region parseBeaconRegion(JSONObject json) throws JSONException, UnsupportedOperationException { String identifier = json.getString("identifier"); //For Android, uuid can be null when scanning for all beacons (I think) String uuid = json.has("uuid") && !json.isNull("uuid") ? json.getString("uuid") : null; String major = json.has("major") && !json.isNull("major") ? json.getString("major") : null; String minor = json.has("minor") && !json.isNull("minor") ? json.getString("minor") : null; if (major == null && minor != null) throw new UnsupportedOperationException("Unsupported combination of 'major' and 'minor' parameters."); Identifier id1 = uuid != null ? Identifier.parse(uuid) : null; Identifier id2 = major != null ? Identifier.parse(major) : null; Identifier id3 = minor != null ? Identifier.parse(minor) : null; return new Region(identifier, id1, id2, id3); } private String nameOfRegionState(int state) { switch (state) { case MonitorNotifier.INSIDE: return "CLRegionStateInside"; case MonitorNotifier.OUTSIDE: return "CLRegionStateOutside"; /*case MonitorNotifier.UNKNOWN: return "CLRegionStateUnknown";*/ default: return "ErrorUnknownCLRegionStateObjectReceived"; } } private JSONObject mapOfRegion(Region region) throws JSONException { //NOTE: NOT SUPPORTING CIRCULAR REGIONS return mapOfBeaconRegion(region); } private JSONObject mapOfBeaconRegion(Region region) throws JSONException { JSONObject dict = new JSONObject(); // identifier if (region.getUniqueId() != null) { dict.put("identifier", region.getUniqueId()); } dict.put("uuid", region.getId1()); if (region.getId2() != null) { dict.put("major", region.getId2()); } if (region.getId3() != null) { dict.put("minor", region.getId3()); } dict.put("typeName", "BeaconRegion"); return dict; } /* NOT SUPPORTED */ /*private JSONObject mapOfCircularRegion(Region region) throws JSONException { JSONObject dict = new JSONObject(); // identifier if (region.getUniqueId() != null) { dict.put("identifier", region.getUniqueId()); } //NOT SUPPORTING CIRCULAR REGIONS //dict.put("radius", region.getRadius()); //JSONObject coordinates = new JSONObject(); //coordinates.put("latitude", 0.0d); //coordinates.put("longitude", 0.0d); //dict.put("center", coordinates); //dict.put("typeName", "CircularRegion"); return dict; }*/ private JSONObject mapOfBeacon(Beacon region) throws JSONException { JSONObject dict = new JSONObject(); //beacon id dict.put("uuid", region.getId1()); dict.put("major", region.getId2()); dict.put("minor", region.getId3()); // proximity dict.put("proximity", nameOfProximity(region.getDistance())); // signal strength and transmission power dict.put("rssi", region.getRssi()); dict.put("tx", region.getTxPower()); // accuracy = rough distance estimate limited to two decimal places (in metres) // NO NOT ASSUME THIS IS ACCURATE - it is effected by radio interference and obstacles dict.put("accuracy", Math.round(region.getDistance() * 100.0) / 100.0); return dict; } private String nameOfProximity(double accuracy) { if (accuracy < 0) { return "ProximityUnknown"; // is this correct? does proximity only show unknown when accuracy is negative? I have seen cases where it returns unknown when // accuracy is -1; } if (accuracy < 0.5) { return "ProximityImmediate"; } // forums say 3.0 is the near/far threshold, but it looks to be based on experience that this is 4.0 if (accuracy <= 4.0) { return "ProximityNear"; } // if it is > 4.0 meters, call it far return "ProximityFar"; } private boolean hasBlueToothPermission() { Context context = cordova.getActivity(); int access = context.checkCallingOrSelfPermission(Manifest.permission.BLUETOOTH); int adminAccess = context.checkCallingOrSelfPermission(Manifest.permission.BLUETOOTH_ADMIN); return (access == PackageManager.PERMISSION_GRANTED) && (adminAccess == PackageManager.PERMISSION_GRANTED); } //////// Async Task Handling //////////////////////////////// private void _handleCallSafely(CallbackContext callbackContext, final ILocationManagerCommand task) { _handleCallSafely(callbackContext, task, true); } private void _handleCallSafely(final CallbackContext callbackContext, final ILocationManagerCommand task, boolean runInBackground) { if (runInBackground) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(final Void... params) { try { _sendResultOfCommand(callbackContext, task.run()); } catch (Exception ex) { _handleExceptionOfCommand(callbackContext, ex); } return null; } }.execute(); } else { try { _sendResultOfCommand(callbackContext, task.run()); } catch (Exception ex) { _handleExceptionOfCommand(callbackContext, ex); } } } private void _handleExceptionOfCommand(CallbackContext callbackContext, Exception exception) { Log.e(TAG, "Uncaught exception: " + exception.getMessage()); Log.e(TAG, "Stack trace: " + exception.getStackTrace()); // When calling without a callback from the client side the command can be null. if (callbackContext == null) { return; } callbackContext.error(exception.getMessage()); } private void _sendResultOfCommand(CallbackContext callbackContext, PluginResult pluginResult) { //debugLog("Send result: " + pluginResult.getMessage()); if (pluginResult.getStatus() != PluginResult.Status.OK.ordinal()) debugWarn("WARNING: " + PluginResult.StatusMessages[pluginResult.getStatus()]); // When calling without a callback from the client side the command can be null. if (callbackContext == null) { return; } callbackContext.sendPluginResult(pluginResult); } private void debugLog(String message) { if (debugEnabled) { Log.d(TAG, message); } } private void debugWarn(String message) { if (debugEnabled) { Log.w(TAG, message); } } //////// IBeaconConsumer implementation ///////////////////// @Override public void onBeaconServiceConnect() { debugLog("Connected to IBeacon service"); } @Override public Context getApplicationContext() { return cordova.getActivity(); } @Override public void unbindService(ServiceConnection connection) { debugLog("Unbind from IBeacon service"); cordova.getActivity().unbindService(connection); } @Override public boolean bindService(Intent intent, ServiceConnection connection, int mode) { debugLog("Bind to IBeacon service"); return cordova.getActivity().bindService(intent, connection, mode); } }