Skip to content

Commit

Permalink
Add support for turning on tracking only after user consent
Browse files Browse the repository at this point in the history
This, combined with
e-mission/e-mission-phone#87
should largely resolve https://github.com/e-mission/e-mission-phone/issues/43

It also provides additional functionality, including for turning off tracking
on re-approval until the user re-consents.

The steps for the changes are:
- Add support for get/set consented to the config manager
- Use that to trigger/defer state machine init on android and iOS
    - on iOS, if not consented, then the data collection plugin is not initialized
    - on android, if not consented, then all `initialize` transitions are ignored, so the state machine is never initialized.
- new javascript interface that indicates when the user has approved, and an
  implementation that saves the consent to the usercache and turns on tracking

On android, we are prompted for re-consent. On iOS, if this is a reconsent, I
think that we will be prompted, because the user has already consented to
receive notifications. But on iOS, if this is the initial consent, and the user
disagrees, then we won't be prompted because the user would not have signed up
for remote notifications.

The inconsistency in prompting is tracked in
e-mission#121
  • Loading branch information
shankari committed Jul 26, 2016
1 parent 8e9a0d3 commit 76c6134
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/android/ConfigManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.HashMap;

import edu.berkeley.eecs.emission.R;
import edu.berkeley.eecs.emission.cordova.tracker.wrapper.ConsentConfig;
import edu.berkeley.eecs.emission.cordova.tracker.wrapper.LocationTrackingConfig;
import edu.berkeley.eecs.emission.cordova.usercache.UserCacheFactory;

Expand Down Expand Up @@ -37,4 +38,15 @@ protected static void updateConfig(Context context, LocationTrackingConfig newCo
.putReadWriteDocument(R.string.key_usercache_sensor_config, newConfig);
cachedConfig = newConfig;
}

public static boolean isConsented(Context context, String reqConsent) {
ConsentConfig currConfig = UserCacheFactory.getUserCache(context)
.getDocument(R.string.key_usercache_consent_config, ConsentConfig.class);
return reqConsent.equals(currConfig.getApproval_date());
}

public static void setConsented(Context context, ConsentConfig newConsent) {
UserCacheFactory.getUserCache(context)
.putReadWriteDocument(R.string.key_usercache_consent_config, newConsent);
}
}
18 changes: 16 additions & 2 deletions src/android/DataCollectionPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Map;

import edu.berkeley.eecs.emission.*;
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.location.TripDiaryStateMachineReceiver;
import edu.berkeley.eecs.emission.cordova.unifiedlogger.Log;
Expand All @@ -32,7 +33,9 @@ public void pluginInitialize() {
final Activity myActivity = cordova.getActivity();
int connectionResult = GooglePlayServicesUtil.isGooglePlayServicesAvailable(myActivity);
if (connectionResult == ConnectionResult.SUCCESS) {
Log.d(myActivity, TAG, "google play services available, initializing state machine");
Log.d(myActivity, TAG, "google play services available, checking consent");
String reqConsent = preferences.getString("emSensorDataCollectionProtocolApprovalDate", "");
if (ConfigManager.isConsented(myActivity, reqConsent)) {
// we want to run this in a separate thread, since it may take some time to get the
// current location and create a geofence
cordova.getThreadPool().execute(new Runnable() {
Expand All @@ -41,18 +44,29 @@ public void run() {
TripDiaryStateMachineReceiver.initOnUpgrade(myActivity);
}
});
};
} else {
Log.e(myActivity, TAG, "unable to connect to google play services");
}
}


@Override
public boolean execute(String action, JSONArray data, final CallbackContext callbackContext) throws JSONException {
if (action.equals("launchInit")) {
Log.d(cordova.getActivity(), TAG, "application launched, init is nop on android");
callbackContext.success();
return true;
} else if (action.equals("markConsented")) {
Log.d(cordova.getActivity(), TAG, "marking consent as done");
Context ctxt = cordova.getActivity();
JSONObject newConsent = data.getJSONObject(0);
ConsentConfig cfg = new Gson().fromJson(newConsent.toString(), ConsentConfig.class);
ConfigManager.setConsented(ctxt, cfg);
// Now, really initialize the state machine
TripDiaryStateMachineReceiver.initOnUpgrade(ctxt);
// TripDiaryStateMachineReceiver.restartCollection(ctxt);
callbackContext.success();
return true;
} else if (action.equals("getConfig")) {
Context ctxt = cordova.getActivity();
LocationTrackingConfig cfg = ConfigManager.getConfig(ctxt);
Expand Down
19 changes: 18 additions & 1 deletion src/android/location/TripDiaryStateMachineReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

import org.apache.cordova.ConfigXmlParser;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
Expand Down Expand Up @@ -38,6 +40,7 @@ public class TripDiaryStateMachineReceiver extends BroadcastReceiver {
public static Set<String> validTransitions = null;
private static String TAG = "TripDiaryStateMachineReceiver";
private static final String SETUP_COMPLETE_KEY = "setup_complete";
private static final int STARTUP_IN_NUMBERS = 7827887;

public TripDiaryStateMachineReceiver() {
// The automatically created receiver needs a default constructor
Expand Down Expand Up @@ -112,7 +115,17 @@ public static void initOnUpgrade(Context ctxt) {

int currentCompleteVersion = sp.getInt(SETUP_COMPLETE_KEY, 0);
if(currentCompleteVersion != BuildConfig.VERSION_CODE) {
Log.d(ctxt, TAG, "Setup not complete, sending initialize");
Log.d(ctxt, TAG, "Setup not complete, checking consent before initialize");
// this is the code that checks whether the native collection has been upgraded and
// restarts the data collection in that case. Without this, tracking is turned off
// until the user restarts the app.
// https://github.com/e-mission/e-mission-data-collection/commit/5544afd64b0c731e1633d1dd9f51a713fdea85fa
// Since every consent change is (presumably) tied to a native code change, we can
// just check for the consent here before reinitializing.
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(ctxt);
String reqConsent = parser.getPreferences().getString("emSensorDataCollectionProtocolApprovalDate", null);
if (ConfigManager.isConsented(ctxt, reqConsent)) {
ctxt.sendBroadcast(new Intent(ctxt.getString(R.string.transition_initialize)));
SharedPreferences.Editor prefsEditor = sp.edit();
// TODO: This is supposed to be set from the javascript as part of the onboarding process.
Expand All @@ -121,6 +134,10 @@ public static void initOnUpgrade(Context ctxt) {
// some questions of the maintainer. For now, setting it here for the first time should be fine.
prefsEditor.putInt(SETUP_COMPLETE_KEY, BuildConfig.VERSION_CODE);
prefsEditor.commit();
} else {
NotificationHelper.createNotification(ctxt, STARTUP_IN_NUMBERS,
"New data collection terms - collection paused until consent");
}
} else {
Log.d(ctxt, TAG, "Setup complete, skipping initialize");
}
Expand Down
27 changes: 27 additions & 0 deletions src/ios/BEMDataCollection.m
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,38 @@ - (void)pluginInitialize
// I tried to move this into a background thread as part of a18f8f9385bdd9e37f7b412b386a911aee9a6ea0 and had
// to revert it because even visit notification, which had been the bedrock of my existence so far, stopped
// working although I made an explicit stop at the education library on the way to Soda.
NSString* reqConsent = self.commandDelegate.settings[@"emSensorDataCollectionProtocolApprovalDate"];
if ([ConfigManager isConsented:reqConsent]) {
self.tripDiaryStateMachine = [TripDiaryStateMachine instance];
} else {
[LocalNotificationManager showNotification:@"New data collection terms - collection paused until consent"];
}
NSDictionary* emptyOptions = @{};
[AppDelegate didFinishLaunchingWithOptions:emptyOptions];
}

- (void)markConsented:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = [command callbackId];
@try {
NSDictionary* newDict = [[command arguments] objectAtIndex:0];
ConsentConfig* newCfg = [ConsentConfig new];
[DataUtils dictToWrapper:newDict wrapper:newCfg];
[ConfigManager setConsented:newCfg];
self.tripDiaryStateMachine = [TripDiaryStateMachine instance];
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
@catch (NSException *exception) {
NSString* msg = [NSString stringWithFormat: @"While updating settings, error %@", exception];
CDVPluginResult* result = [CDVPluginResult
resultWithStatus:CDVCommandStatus_ERROR
messageAsString:msg];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
}
}

- (void)launchInit:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = [command callbackId];
Expand Down
3 changes: 3 additions & 0 deletions src/ios/ConfigManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

#import <Foundation/Foundation.h>
#import "LocationTrackingConfig.h"
#import "ConsentConfig.h"

@interface ConfigManager : NSObject

+ (LocationTrackingConfig*) instance;
+ (void) updateConfig:(LocationTrackingConfig*) newConfig;
+ (BOOL) isConsented:(NSString*)reqConsent;
+ (void) setConsented:(ConsentConfig*) newConfig;
@end
16 changes: 16 additions & 0 deletions src/ios/ConfigManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

#import "ConfigManager.h"
#import "BEMBuiltinUserCache.h"
#import "ConsentConfig.h"

#define SENSOR_CONFIG_KEY @"key.usercache.sensor_config"
#define CONSENT_CONFIG_KEY @"key.usercache.consent_config"

@implementation ConfigManager

Expand Down Expand Up @@ -37,4 +39,18 @@ + (void) updateConfig:(LocationTrackingConfig*) newConfig {
_instance = newConfig;
}

+ (BOOL) isConsented:(NSString*)reqConsent {
ConsentConfig* currConfig = (ConsentConfig*)[[BuiltinUserCache database] getDocument:CONSENT_CONFIG_KEY wrapperClass:[ConsentConfig class]];
if ([reqConsent isEqualToString:currConfig.approval_date]) {
return YES;
} else {
return NO;
}
}

+ (void) setConsented:(ConsentConfig*)newConsent {
[[BuiltinUserCache database] putReadWriteDocument:CONSENT_CONFIG_KEY value:newConsent];
}


@end
5 changes: 5 additions & 0 deletions www/datacollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ var DataCollection = {
launchInit: function (successCallback, errorCallback) {
exec(successCallback, errorCallback, "DataCollection", "launchInit", []);
},
markConsented: function (newConsent) {
return new Promise(function(resolve, reject) {
exec(resolve, reject, "DataCollection", "markConsented", [newConsent]);
});
},
// Switching both the get and set config to a promise to experiment with promises!!
getConfig: function () {
return new Promise(function(resolve, reject) {
Expand Down

0 comments on commit 76c6134

Please sign in to comment.