diff --git a/plugin.xml b/plugin.xml index e94232b3..460e32de 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,7 +1,7 @@ + version="1.8.4"> DataCollection Background data collection FTW! This is the part that I really @@ -81,6 +81,8 @@ + + @@ -183,6 +185,7 @@ + @@ -263,6 +266,7 @@ + @@ -285,6 +289,7 @@ + diff --git a/res/android/statemachine.xml b/res/android/statemachine.xml index aea65112..dec61960 100644 --- a/res/android/statemachine.xml +++ b/res/android/statemachine.xml @@ -11,6 +11,8 @@ local.transition.stopped_moving local.transition.stop_tracking local.transition.start_tracking + local.transition.ble_beacon_found + local.transition.ble_beacon_lost local.transition.tracking_error local.transition.checking_for_beacon local.transition.beacon_found diff --git a/src/android/DataCollectionPlugin.java b/src/android/DataCollectionPlugin.java index 399794e1..231a6805 100644 --- a/src/android/DataCollectionPlugin.java +++ b/src/android/DataCollectionPlugin.java @@ -36,10 +36,12 @@ import edu.berkeley.eecs.emission.cordova.tracker.wrapper.ConsentConfig; import edu.berkeley.eecs.emission.cordova.tracker.wrapper.LocationTrackingConfig; 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; public class DataCollectionPlugin extends CordovaPlugin { public static final String TAG = "DataCollectionPlugin"; @@ -198,6 +200,34 @@ public void run() { } }); return true; + } else if (action.equals("mockBLEObjects")) { + // we want to run this in a background thread because it might sometimes wait to get + // the current location + final String eventType = data.getString(0); + final String uuid = data.getString(1); + final int major = data.getInt(2); + final int minor = data.getInt(3); + final int nObjects = data.getInt(4); + cordova.getThreadPool().execute(new Runnable() { + @Override + public void run() { + Context ctxt = cordova.getActivity(); + for (int i = 0; i < nObjects; i++) { + BluetoothBLE currWrapper = BluetoothBLE.initFake(eventType, uuid, major, minor); + UserCacheFactory.getUserCache(ctxt).putSensorData(R.string.key_usercache_bluetooth_ble, + currWrapper); + } + BluetoothBLE[] justAddedEntries = UserCacheFactory.getUserCache(ctxt).getLastSensorData( + R.string.key_usercache_bluetooth_ble, nObjects, BluetoothBLE.class); + for(BluetoothBLE currEntry : justAddedEntries) { + if (!currEntry.getEventType().equals(eventType)) { + callbackContext.error(currEntry.getEventType()+ " found in last "+nObjects+" objects, expected all "+eventType); + } + callbackContext.success(eventType); + } + } + }); + return true; } else if (action.equals("handleSilentPush")) { throw new UnsupportedOperationException("silent push handling not supported for android"); } else if (action.equals("getAccuracyOptions")) { @@ -225,6 +255,8 @@ private static Map getTransitionMap(Context ctxt) { retVal.put("STOPPED_MOVING", ctxt.getString(R.string.transition_stopped_moving)); retVal.put("STOP_TRACKING", ctxt.getString(R.string.transition_stop_tracking)); retVal.put("START_TRACKING", ctxt.getString(R.string.transition_start_tracking)); + retVal.put("BLE_BEACON_FOUND", ctxt.getString(R.string.transition_ble_beacon_found)); + retVal.put("BLE_BEACON_LOST", ctxt.getString(R.string.transition_ble_beacon_lost)); return retVal; } diff --git a/src/android/location/TripDiaryStateMachineReceiver.java b/src/android/location/TripDiaryStateMachineReceiver.java index ff3c3fb6..7b6ec0dc 100644 --- a/src/android/location/TripDiaryStateMachineReceiver.java +++ b/src/android/location/TripDiaryStateMachineReceiver.java @@ -85,10 +85,9 @@ public void onReceive(Context context, Intent intent) { context.getString(R.string.transition_stopped_moving), context.getString(R.string.transition_stop_tracking), context.getString(R.string.transition_start_tracking), - context.getString(R.string.transition_tracking_error), - context.getString(R.string.transition_checking_for_beacon), - context.getString(R.string.transition_beacon_found), - context.getString(R.string.transition_beacon_not_found) + context.getString(R.string.transition_ble_beacon_found), + context.getString(R.string.transition_ble_beacon_lost), + context.getString(R.string.transition_tracking_error) })); if (!validTransitions.contains(intent.getAction())) { diff --git a/src/android/wrapper/BluetoothBLE.java b/src/android/wrapper/BluetoothBLE.java new file mode 100644 index 00000000..c45391f7 --- /dev/null +++ b/src/android/wrapper/BluetoothBLE.java @@ -0,0 +1,97 @@ +package edu.berkeley.eecs.emission.cordova.tracker.wrapper; + +/** + * Created by shankari on 3/30/24. + */ +public class BluetoothBLE { + public String getEventType() { + return eventType; + } + + /* Not sure if we need to return the UUID, major and minor + since we will only get callbacks for the registered UUID + and we don't need to check the major or minor in native code + only that it is **our** beacon. Let's hold off on them for now. + **/ + + /* We do need to use proximity in the FSM to avoid spurious exits (e.g. a + * personal car parked next to a fleet car may still see a region enter, + * but we don't want to track the trip because the proximity is "far". + */ + public String getProximity() { + return proximity; + } + + public double getTs() { + return ts; + } + + // Similarly, we store the accuracy and the rssi for the record, but the + // underlying API already converts it to a proximity value, so we use that + // instead + + private String eventType; + private String uuid; + private int major; + private int minor; + private String proximity; + private double accuracy; + private int rssi; + + private double ts; + // Should we put newState in here as well? + // If so, we will need to change the location of the save + + private BluetoothBLE() {} + + public static BluetoothBLE initRegionEnter(String uuid, double ts) { + BluetoothBLE enterEvent = new BluetoothBLE(); + enterEvent.eventType = "REGION_ENTER"; + enterEvent.uuid = uuid; + enterEvent.ts = ts; + return enterEvent; + } + + public static BluetoothBLE initRegionExit(String uuid, double ts) { + BluetoothBLE exitEvent = new BluetoothBLE(); + exitEvent.eventType = "REGION_EXIT"; + exitEvent.uuid = uuid; + exitEvent.ts = ts; + return exitEvent; + } + + public static BluetoothBLE initRangeUpdate(String uuid, double ts, + int major, int minor, + String proximity, double accuracy, int rssi) { + BluetoothBLE rangeEvent = new BluetoothBLE(); + rangeEvent.eventType = "RANGE_UPDATE"; + rangeEvent.uuid = uuid; + rangeEvent.ts = ts; + + rangeEvent.major = major; + rangeEvent.minor = minor; + rangeEvent.proximity = proximity; + rangeEvent.accuracy = accuracy; + rangeEvent.rssi = rssi; + return rangeEvent; + } + + public static BluetoothBLE initFake(String eventType, String uuid, int major, int minor) { + BluetoothBLE fakeEvent = new BluetoothBLE(); + fakeEvent.uuid = uuid; + fakeEvent.eventType = eventType; + fakeEvent.ts = System.currentTimeMillis() / 1000; // time is in seconds for us + + // we assume that we don't have major and minor entries for the + // "monitor" responses + if (eventType == "RANGE_UPDATE") { + fakeEvent.major = major; + fakeEvent.minor = minor; + fakeEvent.proximity = "ProximityNear"; + fakeEvent.accuracy = (int)(Math.random() * 100); + fakeEvent.rssi = (int)(Math.random() * 10); + } + + return fakeEvent; + } +} diff --git a/src/ios/BEMDataCollection.h b/src/ios/BEMDataCollection.h index eb1a4e4e..92e621cb 100644 --- a/src/ios/BEMDataCollection.h +++ b/src/ios/BEMDataCollection.h @@ -20,6 +20,7 @@ - (void) setConfig:(CDVInvokedUrlCommand*)command; - (void) getState:(CDVInvokedUrlCommand*)command; - (void) forceTransition:(CDVInvokedUrlCommand *)command; +- (void) mockBLEObjects:(CDVInvokedUrlCommand *)command; - (void) handleSilentPush:(CDVInvokedUrlCommand *)command; - (void)getAccuracyOptions:(CDVInvokedUrlCommand *)command; diff --git a/src/ios/BEMDataCollection.m b/src/ios/BEMDataCollection.m index ba656172..4b486704 100644 --- a/src/ios/BEMDataCollection.m +++ b/src/ios/BEMDataCollection.m @@ -1,6 +1,7 @@ #import "BEMDataCollection.h" #import "LocalNotificationManager.h" #import "Wrapper/LocationTrackingConfig.h" +#import "Wrapper/BluetoothBLE.h" #import "BEMAppDelegate.h" #import "ConfigManager.h" #import "DataUtils.h" @@ -301,6 +302,54 @@ - (void)forceTransition:(CDVInvokedUrlCommand *)command messageAsString:msg]; [self.commandDelegate sendPluginResult:result callbackId:callbackId]; } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } +} + +- (void)mockBLEObjects:(CDVInvokedUrlCommand *)command +{ + NSString* callbackId = [command callbackId]; + + @try { + NSString* eventType = [[command arguments] objectAtIndex:0]; + NSString* uuid = [[command arguments] objectAtIndex:1]; + int major = [[[command arguments] objectAtIndex:2] intValue]; + int minor = [[[command arguments] objectAtIndex:3] intValue]; + int nObjects = [[[command arguments] objectAtIndex:4] intValue]; + for (int i = 0; i < nObjects; i++) { + BluetoothBLE* currWrapper = [[BluetoothBLE new] initFake:@"RANGE_UPDATE" anduuid: uuid andmajor: major andminor: minor]; + [[BuiltinUserCache database] putSensorData:@"key.usercache.bluetooth_ble" value:currWrapper]; + } + NSArray* justAddedEntries = [[BuiltinUserCache database] getLastSensorData:@"key.usercache.bluetooth_ble" + nEntries:nObjects + wrapperClass:BluetoothBLE.class]; + for (int i = 0; i < justAddedEntries.count; i++) { + BluetoothBLE* currEntry = [justAddedEntries objectAtIndex:i]; + if (![currEntry.eventType isEqualToString:eventType]) { + NSString* msg = [NSString stringWithFormat: @"%@ found in last %d objects, expected all %@", currEntry.eventType, nObjects, eventType]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + } + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK + messageAsString:eventType]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } + @catch (NSException *exception) { + NSString* msg = [NSString stringWithFormat: @"While getting settings, error %@", exception]; + CDVPluginResult* result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_ERROR + messageAsString:msg]; + [self.commandDelegate sendPluginResult:result callbackId:callbackId]; + } } - (void)handleSilentPush:(CDVInvokedUrlCommand *)command @@ -367,6 +416,8 @@ - (NSDictionary*) getTransitionMap { retVal[@"RECEIVED_SILENT_PUSH"] = CFCTransitionRecievedSilentPush; retVal[@"VISIT_STARTED"] = CFCTransitionVisitStarted; retVal[@"VISIT_ENDED"] = CFCTransitionVisitEnded; + retVal[@"BLE_BEACON_FOUND"] = CFCTransitionBeaconFound; + retVal[@"BLE_BEACON_LOST"] = CFCTransitionBeaconLost; return retVal; } diff --git a/src/ios/Location/TripDiaryStateMachine.h b/src/ios/Location/TripDiaryStateMachine.h index 2dd99de1..c97740f7 100644 --- a/src/ios/Location/TripDiaryStateMachine.h +++ b/src/ios/Location/TripDiaryStateMachine.h @@ -37,6 +37,8 @@ #define CFCTransitionStartTracking @"T_START_TRACKING" #define CFCTransitionVisitStarted @"T_VISIT_STARTED" #define CFCTransitionVisitEnded @"T_VISIT_ENDED" +#define CFCTransitionBeaconFound @"T_BLE_BEACON_FOUND" +#define CFCTransitionBeaconLost @"T_BLE_BEACON_LOST" #define CFCTransitionNOP @"T_NOP" /* diff --git a/src/ios/Wrapper/BluetoothBLE.h b/src/ios/Wrapper/BluetoothBLE.h new file mode 100644 index 00000000..e4b1d8b6 --- /dev/null +++ b/src/ios/Wrapper/BluetoothBLE.h @@ -0,0 +1,28 @@ +// +// Transition.h +// CFC_Tracker +// +// Created by Kalyanaraman Shankari on 10/27/15. +// Copyright © 2015 Kalyanaraman Shankari. All rights reserved. +// + +#import +#import + +@interface BluetoothBLE : NSObject + +- (instancetype)initWithCLBeacon:(CLBeacon *)beacon andEventType:(NSString *) eventType; +- (instancetype)initFake:(NSString *)eventType anduuid:(NSString*) uuid andmajor:(int) major andminor:(int) minor; + +// fields from CLBeacon, modified to be easy to serialize and restore +@property NSString* uuid; +@property NSInteger major; +@property NSInteger minor; +@property NSString* proximity; +@property CLLocationAccuracy accuracy; +@property NSInteger rssi; +@property NSString* eventType; +@property double ts; + ++ (NSString*) proximityToString:(CLProximity) proximityObj; +@end diff --git a/src/ios/Wrapper/BluetoothBLE.m b/src/ios/Wrapper/BluetoothBLE.m new file mode 100644 index 00000000..2de61cf5 --- /dev/null +++ b/src/ios/Wrapper/BluetoothBLE.m @@ -0,0 +1,54 @@ +// +// Transition.m +// CFC_Tracker +// +// Created by Kalyanaraman Shankari on 10/27/15. +// Copyright © 2015 Kalyanaraman Shankari. All rights reserved. +// + +#import "BluetoothBLE.h" +#import "DataUtils.h" +#import + +@implementation BluetoothBLE + +-(id) initWithCLBeacon:(CLBeacon*) beacon andEventType:(NSString*) eventType { + self = [super init]; + self.uuid = beacon.UUID; + self.major = beacon.major.integerValue; + self.minor = beacon.minor.integerValue; + self.proximity = [BluetoothBLE proximityToString:beacon.proximity]; + self.accuracy = beacon.accuracy; + self.rssi = beacon.rssi; + + self.eventType = eventType; + self.ts = [DataUtils dateToTs:beacon.timestamp]; + return self; +} + +-(id) initFake:(NSString*) eventType anduuid:(NSString*) uuid andmajor:(int) major andminor:(int)minor { + self = [super init]; + self.uuid = uuid; + self.eventType = eventType; + self.ts = [DataUtils dateToTs:[NSDate now]]; + + if ([eventType isEqualToString:@"RANGE_UPDATE"]) { + self.major = major; + self.minor = minor; + self.proximity = [BluetoothBLE proximityToString:CLProximityNear]; + self.accuracy = arc4random_uniform(100); + self.rssi = arc4random_uniform(10); + } + + return self; +} + ++ (NSString*) proximityToString:(CLProximity) proximityObj { + if (proximityObj == CLProximityImmediate) { return @"ProximityImmediate"; }; + if (proximityObj == CLProximityNear) { return @"ProximityNear"; }; + if (proximityObj == CLProximityFar) { return @"ProximityFar"; }; + return @"ProximityUnknown"; +} + + +@end diff --git a/www/datacollection.js b/www/datacollection.js index b1820ab7..a54ae313 100644 --- a/www/datacollection.js +++ b/www/datacollection.js @@ -127,6 +127,15 @@ var DataCollection = { exec(resolve, reject, "DataCollection", "forceTransition", [generalTransitionName]); }); }, + mockBLEObjects: function (eventType, uuid, major, minor, nObjects) { + // major and minor are optional, so we check if they are defined and + // put in a default value if they don't this allows us to have a nice + // external interface without running into null pointers internally + return new Promise(function(resolve, reject) { + exec(resolve, reject, "DataCollection", "mockBLEObjects", [eventType, uuid, + major? major : -1, minor? minor: -1, nObjects]); + }); + }, handleSilentPush: function() { return new Promise(function(resolve, reject) { exec(resolve, reject, "DataCollection", "handleSilentPush",