diff --git a/plugin.xml b/plugin.xml index 6b25cf4..31a2d71 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.8.5"> DataCollection Background data collection FTW! This is the part that I really @@ -40,6 +40,7 @@ + @@ -73,7 +74,7 @@ + android:exported="true"> @@ -126,6 +127,16 @@ android:enabled="true" android:exported="false"> + + + + @@ -175,6 +186,8 @@ + + diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index c0b45d2..231a680 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -38,6 +38,7 @@ import edu.berkeley.eecs.emission.cordova.tracker.wrapper.StatsEvent; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.BluetoothBLE; import edu.berkeley.eecs.emission.cordova.tracker.verification.SensorControlForegroundDelegate; +import edu.berkeley.eecs.emission.cordova.tracker.bluetooth.BluetoothService; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.usercache.BuiltinUserCache; import edu.berkeley.eecs.emission.cordova.usercache.UserCacheFactory; @@ -61,6 +62,10 @@ public void pluginInitialize() { new StatsEvent(myActivity, R.string.app_launched)); TripDiaryStateMachineReceiver.initOnUpgrade(myActivity); + + // Ask for bluetooth permissions + // We will change this with future releases, we just ran out of time implementing this into the front end + mControlDelegate.checkAndPromptBluetoothScanPermissions(); } @Override @@ -233,6 +238,12 @@ public void run() { retVal.put("PRIORITY_NO_POWER", LocationRequest.PRIORITY_NO_POWER); callbackContext.success(retVal); return true; + } else if (action.equals("bluetoothScan")) { + Context ctxt = cordova.getActivity(); + Log.d(ctxt, TAG, "JS requested scan for bluetooth!"); + Intent bluetoothServiceIntent = new Intent(ctxt, BluetoothService.class); + ctxt.startService(bluetoothServiceIntent); + return true; } return false; } diff --git a/src/android/bluetooth/BluetoothMonitoringService.java b/src/android/bluetooth/BluetoothMonitoringService.java new file mode 100644 index 0000000..7cc0f6a --- /dev/null +++ b/src/android/bluetooth/BluetoothMonitoringService.java @@ -0,0 +1,86 @@ +package edu.berkeley.eecs.emission.cordova.tracker.bluetooth; + +// Android imports +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +// Altbeacon imports +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.Region; +import org.altbeacon.beacon.MonitorNotifier; +import org.altbeacon.beacon.Beacon; + +// Other plugin imports +import edu.berkeley.eecs.emission.R; +import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; +import edu.berkeley.eecs.emission.cordova.tracker.verification.SensorControlChecks; + + +public class BluetoothMonitoringService extends Service { + private static String TAG = "BluetoothMonitoringService"; + private BeaconManager beaconManager; + private String uuid = "bf3df3b1-6e46-35fa-86e5-927c95dd096c"; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(this, TAG, "onStartCommand called!!!!"); + + // Instantiate variable + beaconManager = BeaconManager.getInstanceForApplication(this); + // This line will ensure that we always get a first callback if beacon is in region when we start monitoring + // https://github.com/AltBeacon/android-beacon-library/issues/708#issuecomment-399513853 + beaconManager.setRegionStatePersistenceEnabled(false); + + // Start monitoring for BLE beacons + startMonitoring(); + + // Start sticky + return 1; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void startMonitoring() { + Log.d(this, TAG, "Start monitoring has been called!"); + + // Code to start monitoring for BLE beacons using AltBeacon library + beaconManager.addMonitorNotifier(new MonitorNotifier() { + @Override + public void didEnterRegion(Region region) { + Log.d(BluetoothMonitoringService.this, TAG, "I just saw a beacon for the first time!"); + } + + @Override + public void didExitRegion(Region region) { + Log.d(BluetoothMonitoringService.this, TAG, "I no longer see an beacon"); + } + + @Override + public void didDetermineStateForRegion(int state, Region region) { + Log.d(BluetoothMonitoringService.this, TAG, "I have just switched from seeing/not seeing beacons: "+state); + } + }); + + // Define our region and start monitoring + Region region = new Region(uuid, null, null, null); + beaconManager.startMonitoring(region); + Log.d(this, TAG, "Starting to monitor for our region: " + region.toString()); + } + + @Override + public void onDestroy() { + stopMonitoring(); + super.onDestroy(); + } + + private void stopMonitoring() { + Log.d(this, TAG, "Stopping monitoring for the beacon."); + beaconManager.stopMonitoring(new Region(uuid, null, null, null)); + beaconManager.removeAllMonitorNotifiers(); + } +} diff --git a/src/android/bluetooth/BluetoothService.java b/src/android/bluetooth/BluetoothService.java new file mode 100644 index 0000000..79af160 --- /dev/null +++ b/src/android/bluetooth/BluetoothService.java @@ -0,0 +1,137 @@ +package edu.berkeley.eecs.emission.cordova.tracker.bluetooth; + +// Android imports +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import java.util.Collection; +import java.util.Set; +import java.util.HashSet; + +// Altbeacon imports +import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.Region; +import org.altbeacon.beacon.RangeNotifier; +import org.altbeacon.beacon.Beacon; + +// Other plugin imports +import edu.berkeley.eecs.emission.R; +import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; +import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; +import edu.berkeley.eecs.emission.cordova.tracker.verification.SensorControlChecks; + +// Saving data +import edu.berkeley.eecs.emission.cordova.usercache.UserCacheFactory; +import edu.berkeley.eecs.emission.cordova.tracker.wrapper.BluetoothBLE; + +public class BluetoothService extends Service { + private static String TAG = "BluetoothService"; + private BeaconManager beaconManager; + private Set scanned; + private String uuid = "bf3df3b1-6e46-35fa-86e5-927c95dd096c"; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(this, TAG, "onStartCommand called!!!!"); + + // Instantiate variables + beaconManager = BeaconManager.getInstanceForApplication(this); + scanned = new HashSet<>(); + + // Check to see if we even have permission to scan at all + boolean bluetoothPermissions = SensorControlChecks.checkBluetoothScanningPermissions(this); + + if (!bluetoothPermissions) { + return 1; + } + + // Start scanning for BLE beacons + startBeaconScan(); + + // Start sticky + return 1; + } + + // @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void isInRange() { + Log.d(this, TAG, "Is in range has been called!"); + + stopBeaconScan(); + + Log.d(this, TAG, "Done waiting, results are... " + scanned.size()); + + if (scanned.size() > 0) { + Log.d(this, TAG, "Found something!"); + Log.d(this, TAG, scanned.toString()); + this.sendBroadcast(new ExplicitIntent(this, R.string.transition_ble_beacon_found)); + } + onDestroy(); + } + + private void startBeaconScan() { + // Code to start scanning for BLE beacons using AltBeacon library + Log.d(this, TAG, "startBeaconScan called!!!!"); + + // Keep track of how many times we have scanned + beaconManager.addRangeNotifier(new RangeNotifier() { + int numScans = 0; + + @Override + public void didRangeBeaconsInRegion(Collection beacons, Region region) { + Log.d(BluetoothService.this, TAG, "Range notifier called..."); + + if (beacons.size() > 0) { + Log.d(BluetoothService.this, TAG, "Found beacons " + beacons.toString()); + + for (Beacon beacon : beacons) { + // Even though we are scanning for beacons in a certain region, beacons with different UUID's still come up. + // Until we figure out why that is the case, put this check in place so we only save the ones with the right UUID. + if (beacon.getId1().toString().equals(uuid)) { + scanned.add(beacon); + BluetoothBLE currWrapper = BluetoothBLE.initRangeUpdate( + beacon.getId1().toString(), + System.currentTimeMillis() / 1000, // timestamp in always in secs for us + beacon.getId2().toInt(), + beacon.getId3().toInt(), +// TODO: Figure out what to do with the distance calculations + "ProximityNear", +// 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 +// from https://github.com/petermetz/cordova-plugin-ibeacon + Math.round((beacon.getDistance() * 100)/100), + beacon.getRssi()); + UserCacheFactory.getUserCache(BluetoothService.this) + .putSensorData(R.string.key_usercache_bluetooth_ble, currWrapper); + } + } + } + + numScans++; + + if (numScans >= 4) { + // Once we have hit certain number of scans, stop and determine if any beacons are in range + isInRange(); + } + } + }); + + beaconManager.startRangingBeacons(new Region(uuid, null, null, null)); + } + + @Override + public void onDestroy() { + // stopBeaconScan(); + super.onDestroy(); + } + + private void stopBeaconScan() { + Log.d(this, TAG, "Stopping monitoring for the beacon."); + beaconManager.stopRangingBeacons(new Region(uuid, null, null, null)); + beaconManager.removeAllRangeNotifiers(); + } +} diff --git a/src/android/e-mission-datacollection.gradle b/src/android/e-mission-datacollection.gradle new file mode 100644 index 0000000..068742c --- /dev/null +++ b/src/android/e-mission-datacollection.gradle @@ -0,0 +1,7 @@ +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.altbeacon:android-beacon-library:2.19' +} \ No newline at end of file diff --git a/src/android/location/TripDiaryStateMachineForegroundService.java b/src/android/location/TripDiaryStateMachineForegroundService.java index 9e1dabc..36982cd 100644 --- a/src/android/location/TripDiaryStateMachineForegroundService.java +++ b/src/android/location/TripDiaryStateMachineForegroundService.java @@ -29,6 +29,8 @@ import edu.berkeley.eecs.emission.cordova.tracker.verification.SensorControlBackgroundChecker; import edu.berkeley.eecs.emission.cordova.tracker.ExplicitIntent; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.StatsEvent; +import edu.berkeley.eecs.emission.cordova.tracker.bluetooth.BluetoothService; +import edu.berkeley.eecs.emission.cordova.tracker.bluetooth.BluetoothMonitoringService; import edu.berkeley.eecs.emission.cordova.usercache.BuiltinUserCache; import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; @@ -79,6 +81,19 @@ public static String humanizeState(Context ctxt, String state) { public int onStartCommand(Intent intent, int flags, int startId) { Log.d(this, TAG, "onStartCommand called with intent = "+intent+ " flags = " + flags + " and startId = " + startId); + + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals("foreground_start_bluetooth")) { + Intent bluetoothService = new Intent(this, BluetoothService.class); + this.startService(bluetoothService); + return START_STICKY; + } else if (intent.getAction().equals("foreground_start_bluetooth_monitoring")) { + Intent bluetoothService = new Intent(this, BluetoothMonitoringService.class); + this.startService(bluetoothService); + return START_STICKY; + } + } + String message = humanizeState(this, TripDiaryStateMachineService.getState(this)); if (intent == null) { SensorControlBackgroundChecker.checkAppState(this); diff --git a/src/android/location/TripDiaryStateMachineService.java b/src/android/location/TripDiaryStateMachineService.java index dd300d3..cfde9d8 100644 --- a/src/android/location/TripDiaryStateMachineService.java +++ b/src/android/location/TripDiaryStateMachineService.java @@ -32,6 +32,8 @@ import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log; import edu.berkeley.eecs.emission.cordova.usercache.UserCacheFactory; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.Transition; +import edu.berkeley.eecs.emission.cordova.tracker.bluetooth.BluetoothService; +import edu.berkeley.eecs.emission.cordova.tracker.location.TripDiaryStateMachineForegroundService; /** * Created by shankari on 9/12/15. @@ -47,6 +49,8 @@ public class TripDiaryStateMachineService extends Service { private String mTransition = null; private SharedPreferences mPrefs = null; private ForegroundServiceComm mComm = null; + private JSONObject config; + private boolean isFleet = false; public TripDiaryStateMachineService() { super(); @@ -59,6 +63,15 @@ public void onCreate() { /* * Need to initialize once per create. */ + try { + JSONObject c = (JSONObject) UserCacheFactory.getUserCache(this).getDocument("config/app_ui_config", false); + config = c; + isFleet = (config != null && config.has("tracking") && config.getJSONObject("tracking").getBoolean("bluetooth_only")); + } catch (JSONException e) { + Log.d(this, TAG, "Error reading config! " + e); + // TODO: Need to figure out what to do about the fleet flag when the config is invalid + // Original implementation by @louisg1337 had isFleet = true in that case (location tracking would not stop) + } } @Override @@ -163,6 +176,7 @@ private void handleAction(Context ctxt, String currState, String actionString) { Log.i(this, TAG, "JSONException while accessing geofence cfg "+ " skipping delete"); } + if (actionString.equals(ctxt.getString(R.string.transition_initialize))) { handleStart(ctxt, actionString); } else if (currState.equals(ctxt.getString(R.string.state_start))) { @@ -186,6 +200,21 @@ private void handleStart(final Context ctxt, String actionString) { // create the significant motion callback first since it is // synchronous and the geofence is not createGeofenceInThread(ctxt, actionString); + + // // Comment this out for now as this interferes with our beacon ranging function + // // Check to see if we are in fleet mode, if we are then start monitoring for beacons + // try { + // if (config != null && config.has("tracking") && config.getJSONObject("tracking").getBoolean("bluetooth_only")){ + // // Send intent to foreground service to start monitoring service + // Log.d(this, TAG, "Starting the bluetooth monitoring!"); + // Intent foregroundBluetoothMonitoring = new Intent(ctxt, TripDiaryStateMachineForegroundService.class); + // foregroundBluetoothMonitoring.setAction("foreground_start_bluetooth_monitoring"); + // ctxt.startService(foregroundBluetoothMonitoring); + // } + // } catch (JSONException e) { + // Log.d(this, TAG, "Error trying to read config to monitor for beacons!"); + // } + return; // we will wait for async geofence creation to complete } @@ -221,7 +250,45 @@ private void handleStart(final Context ctxt, String actionString) { public void handleTripStart(Context ctxt, final String actionString) { Log.d(this, TAG, "TripDiaryStateMachineReceiver handleTripStart(" + actionString + ") called"); - if (actionString.equals(ctxt.getString(R.string.transition_exited_geofence))) { + /* + * The logic here is simple. + * If we get a geofence exit, we have started moving. Since we are + * currently only addressing the uncommon case, we will check to see if + * we find a beacon. If so, we start location tracking. If not, we don't. + */ + + if (actionString.equals(ctxt.getString(R.string.transition_exited_geofence)) || actionString.equals(ctxt.getString(R.string.transition_ble_beacon_found))) { + // Since we only start bluetooth scanning after geofence exit now, + // we will never get a ble_beacon_found before the geofence exit + if (actionString.equals(ctxt.getString(R.string.transition_exited_geofence))) { + // we have a geofence exit, so we have started moving + if (isFleet) { + // fleet version, so we start ranging for bluetooth + Log.d(this, TAG, "Geofence exit in fleet mode, checking for beacons before starting location tracking"); + // Start up the bluetooth service to check for beacons + Intent foregroundStartBluetooth = new Intent(ctxt, TripDiaryStateMachineForegroundService.class); + foregroundStartBluetooth.setAction("foreground_start_bluetooth"); + ctxt.startService(foregroundStartBluetooth); + return; + } else { + Log.d(this, TAG, "Geofence exit in non-fleet mode, starting location tracking"); + } + } + + if (actionString.equals(ctxt.getString(R.string.transition_ble_beacon_found))) { + // we have found a BLE beacon. we only start ranging for BLE beacons in fleet mode + // so we should be in fleet mode here + if (isFleet) { + Log.d(this, TAG, "Found beacon in fleet mode, starting location tracking"); + } else { + Log.e(this, TAG, "Found beacon in non-fleet mode, not sure why this happened, starting location tracking anyway"); + } + } + + // we will get here as long as we don't return, so all the "starting location tracking" will work properly + // but this is ugly + // TODO: Pull this out to a separate function and call the function from the if/then blocks + // Delete geofence // we cannot add null elements to the token list. // the LocationTracking start action can now return null @@ -279,7 +346,7 @@ public void handleTripStart(Context ctxt, final String actionString) { if (ConfigManager.getConfig(fCtxt).isSimulateUserInteraction()) { NotificationHelper.createNotification(fCtxt, STATE_IN_NUMBERS, null, fCtxt.getString(R.string.failed_moving_new_state,newState)); - } // both branches have called setState or are waiting for sth else + } // both branches have called setState or are waiting for sth else }); // listener end return; // handled the transition, returning } diff --git a/src/android/verification/SensorControlChecks.java b/src/android/verification/SensorControlChecks.java index 287c628..c14fece 100644 --- a/src/android/verification/SensorControlChecks.java +++ b/src/android/verification/SensorControlChecks.java @@ -7,6 +7,7 @@ import android.location.Location; import android.os.Build; import android.os.PowerManager; +import android.content.pm.PackageManager; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationManagerCompat; @@ -130,4 +131,14 @@ public static boolean checkIgnoreBatteryOptimizations(final Context ctxt) { PowerManager pm = (PowerManager)ctxt.getSystemService(Context.POWER_SERVICE); return pm.isIgnoringBatteryOptimizations(ctxt.getPackageName()); } + + public static boolean checkBluetoothScanningPermissions(final Context ctxt) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S){ + // If its an older build version than API 31, we don't have to worry about scanning permissions + return true; + } else { + int permissionState = ctxt.checkSelfPermission("android.permission.BLUETOOTH_SCAN"); + return permissionState == PackageManager.PERMISSION_GRANTED; + } + } } diff --git a/src/android/verification/SensorControlForegroundDelegate.java b/src/android/verification/SensorControlForegroundDelegate.java index 298d7fb..aa865ce 100644 --- a/src/android/verification/SensorControlForegroundDelegate.java +++ b/src/android/verification/SensorControlForegroundDelegate.java @@ -420,6 +420,11 @@ public void checkAndPromptLocationPermissions(CallbackContext cordovaCallback) { } } + /** + * Check to see if the user has the ability to scan for bluetooth devices, if not prompt them asking for it. + * + * @param cordovaCallback + */ public void checkAndPromptBluetoothScanPermissions(CallbackContext cordovaCallback) { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S){ Log.d(cordova.getActivity(), TAG, "Older build version than API 31, return success!"); @@ -439,6 +444,25 @@ public void checkAndPromptBluetoothScanPermissions(CallbackContext cordovaCallba } } + /** + * Overloaded version of function aboe so we can use on native side. + */ + public void checkAndPromptBluetoothScanPermissions() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S){ + Log.d(cordova.getActivity(), TAG, "Older build version than API 31, return success!"); + } else if (cordova.hasPermission(SensorControlConstants.BLUETOOTH_SCAN)){ + Log.d(cordova.getActivity(), TAG, "User has already enabled bluetooth scan!"); + } else { + Log.d(cordova.getActivity(), TAG, "User has not enabled bluetooth scan, requesting now..."); + this.permissionChecker = getPermissionChecker( + SensorControlConstants.ENABLE_BLUETOOTH_SCAN, + SensorControlConstants.BLUETOOTH_SCAN, + "Please enable \'Nearby devices\' permission to use the scanner.", + "Please enable \'Nearby devices\' permission to use the scanner."); + this.permissionChecker.requestPermission(); + } + } + public void checkMotionActivityPermissions(CallbackContext cordovaCallback) { boolean validPerms = SensorControlChecks.checkMotionActivityPermissions(cordova.getActivity()); if(validPerms) { @@ -667,6 +691,11 @@ public void onRequestPermissionResult(int requestCode, String[] permissions, } break; case SensorControlConstants.ENABLE_BLUETOOTH_SCAN: + if (cordovaCallback == null) { + this.permissionChecker = null; + break; + } + Log.d(cordova.getActivity(), TAG, "Got return for bluetooth scanning permission..."); if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(cordova.getActivity(), TAG, "Bluetooth scanning is allowed!"); @@ -786,6 +815,10 @@ public void run() { } }); case SensorControlConstants.ENABLE_BLUETOOTH_SCAN: + if (cordovaCallback == null) { + break; + } + Log.d(mAct, TAG, requestCode + " is our code, handling callback"); Log.d(mAct, TAG, "Got bluetooth callback from launching app settings"); if (cordova.hasPermission(SensorControlConstants.BLUETOOTH_SCAN)) { diff --git a/www/datacollection.js b/www/datacollection.js index a7ef0fa..a54ae31 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -141,6 +141,11 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "handleSilentPush", []); }) + }, + bluetoothScan: function() { + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "bluetoothScan", []); + }); } }