Skip to content

Commit a09385b

Browse files
committed
Switch to primarily native storage
Back in the old days, we tried to move all storage to local storage so that we could read it without waiting for ionicPlatform.ready and startup quickly. Well, it turned out that sometimes local storage was deleted on iOS, so we ended up with the case where local intro_done was deleted, so we would reset the UI. Which looked stupid and like a bug (https://github.com/e-mission/e-mission-phone/issues/443). Re-writing to pull out the KV storage into a service, write primarily to native with local as backup, and read from both and unify. Everything was already promisified because we were reading consent from native code, so I am not sure that this actually affects performance. And it is a heck of a lot easier to understand. Testing done: - started app -> went to intro - restarted app after restarting emulator -> went to current screen. For fixes to the current screen, see 0ecfdae - cleared out intro_done and consent from local storage and restarted app -> went to current screen - cleared all native storage, generated popups for intro_done and consent, then crashed due to e-mission/e-mission-transition-notify#18 On relaunch, launched properly without any popups. I declare that this is done. ``` [phonegap] [console.log] DEBUG:uc_stored_val = null ls_stored_val = {"intro_done":"2018-09-25T13:14:51-07:00"} ```
1 parent 0ecfdae commit a09385b

File tree

3 files changed

+156
-114
lines changed

3 files changed

+156
-114
lines changed

www/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
<!--script src="js/goals/party.js"></script-->
9999
<script src="js/goals/signup.js"></script>
100100
<script src="js/plugin/logger.js"></script>
101+
<script src="js/plugin/storage.js"></script>
101102

102103

103104
<script src="lib/d3/d3.js"></script>

www/js/plugin/storage.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
angular.module('emission.plugin.kvstore', ['emission.plugin.logger',
2+
'angularLocalStorage'])
3+
4+
.factory('KVStore', function($window, Logger, storage, $ionicPopup) {
5+
var logger = Logger;
6+
var kvstoreJs = {}
7+
/*
8+
* Sets in both localstorage and native storage
9+
* If the message is not a JSON object, wrap it in an object with the key
10+
* "value" before storing it.
11+
*/
12+
var getNativePlugin = function() {
13+
return $window.cordova.plugins.BEMUserCache;
14+
}
15+
16+
kvstoreJs.set = function(key, value) {
17+
// add checks for data type
18+
var store_val = value;
19+
if (typeof value != "object") {
20+
// Should this be {"value": value} or {key: value}?
21+
store_val = {};
22+
store_val[key] = value;
23+
}
24+
/*
25+
* How should we deal with consistency here? Have the threads be
26+
* independent so that there is greater chance that one will succeed,
27+
* or the local only succeed if native succeeds. I think parallel is
28+
* better for greater robustness.
29+
*/
30+
storage.set(key, store_val);
31+
return getNativePlugin().putLocalStorage(key, store_val);
32+
}
33+
34+
var getUnifiedValue = function(key) {
35+
var ls_stored_val = storage.get(key, undefined);
36+
return getNativePlugin().getLocalStorage(key, false).then(function(uc_stored_val) {
37+
logger.log("uc_stored_val = "+JSON.stringify(uc_stored_val)+" ls_stored_val = "+JSON.stringify(ls_stored_val));
38+
if (angular.equals(ls_stored_val, uc_stored_val)) {
39+
logger.log("local and native values match, already synced");
40+
return uc_stored_val;
41+
} else {
42+
// the values are different
43+
if (ls_stored_val == null) {
44+
console.assert(uc_stored_val != null, "uc_stored_val should be non-null");
45+
logger.log("uc_stored_val = "+JSON.stringify(uc_stored_val)+
46+
" ls_stored_val = "+JSON.stringify(ls_stored_val)+
47+
" copying native "+key+" to local...");
48+
storage.set(key, uc_stored_val);
49+
return uc_stored_val;
50+
} else if (uc_stored_val == null) {
51+
console.assert(ls_stored_val != null);
52+
$ionicPopup.alert({template: "Local "+key+" found, native "
53+
+key+" missing, writing "+key+" to native"})
54+
logger.log("uc_stored_val = "+JSON.stringify(uc_stored_val)+
55+
" ls_stored_val = "+JSON.stringify(ls_stored_val)+
56+
" copying local "+key+" to native...");
57+
return getNativePlugin().putLocalStorage(key, ls_stored_val).then(function() {
58+
// we only return the value after we have finished writing
59+
return ls_stored_val;
60+
});
61+
}
62+
console.assert(ls_stored_val != null && uc_stored_val != null,
63+
"ls_stored_val ="+JSON.stringify(ls_stored_val)+
64+
"uc_stored_val ="+JSON.stringify(uc_stored_val));
65+
$ionicPopup.alert({template: "Local "+key+" found, native "
66+
+key+" found, but different, writing "+key+" to local"})
67+
logger.log("uc_stored_val = "+JSON.stringify(uc_stored_val)+
68+
" ls_stored_val = "+JSON.stringify(ls_stored_val)+
69+
" copying native "+key+" to local...");
70+
storage.set(key, uc_stored_val);
71+
return uc_stored_val;
72+
}
73+
});
74+
}
75+
76+
kvstoreJs.get = function(key) {
77+
return getUnifiedValue(key).then(function(retData) {
78+
if((retData != null) && (angular.isDefined(retData[key]))) {
79+
// it must have been a simple data type that we munged upfront
80+
return retData[key];
81+
} else {
82+
// it must have been an object
83+
return retData;
84+
}
85+
});
86+
}
87+
88+
kvstoreJs.remove = function(key) {
89+
storage.remove(key);
90+
return getNativePlugin().removeLocalStorage(key);
91+
}
92+
93+
return kvstoreJs;
94+
});

www/js/splash/startprefs.js

Lines changed: 61 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
angular.module('emission.splash.startprefs', ['emission.plugin.logger',
22
'emission.splash.referral',
3+
'emission.plugin.kvstore',
34
'angularLocalStorage'])
45

56
.factory('StartPrefs', function($window, $state, $interval, $rootScope, $ionicPlatform,
6-
$ionicPopup, storage, $http, Logger, ReferralHandler) {
7+
$ionicPopup, KVStore, storage, $http, Logger, ReferralHandler) {
78
var logger = Logger;
89
var nTimesCalled = 0;
910
var startprefs = {};
@@ -47,11 +48,11 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger',
4748

4849
startprefs.markConsented = function() {
4950
logger.log("changing consent from "+
50-
$rootScope.curr_consented+" -> "+$rootScope.req_consent);
51+
$rootScope.curr_consented+" -> "+JSON.stringify($rootScope.req_consent));
5152
// mark in native storage
5253
return startprefs.readConsentState().then(writeConsentToNative).then(function(response) {
5354
// mark in local storage
54-
storage.set(DATA_COLLECTION_CONSENTED_PROTOCOL,
55+
KVStore.set(DATA_COLLECTION_CONSENTED_PROTOCOL,
5556
$rootScope.req_consent);
5657
// mark in local variable as well
5758
$rootScope.curr_consented = angular.copy($rootScope.req_consent);
@@ -60,72 +61,57 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger',
6061
};
6162

6263
startprefs.markIntroDone = function() {
63-
storage.set(INTRO_DONE_KEY, true);
64-
// Need to initialize this first because if we try to
65-
// create it inlike with {key: value}, the key becomes the
66-
// word "INTRO_DONE_KEY" and the stored object is
67-
// {"INTRO_DONE_KEY":"2018-01-31T06:26:02+00:00"}
68-
var to_store = {};
69-
to_store[INTRO_DONE_KEY] = moment().format();
70-
$window.cordova.plugins.BEMUserCache.putLocalStorage(INTRO_DONE_KEY, to_store);
71-
$rootScope.$emit(startprefs.INTRO_DONE_EVENT, $rootScope.req_consent);
64+
var currTime = moment().format();
65+
KVStore.set(INTRO_DONE_KEY, currTime);
66+
$rootScope.$emit(startprefs.INTRO_DONE_EVENT, currTime);
7267
}
7368

7469
// returns boolean
70+
startprefs.readIntroDone = function() {
71+
return KVStore.get(INTRO_DONE_KEY).then(function(read_val) {
72+
logger.log("in readIntroDone, read_val = "+JSON.stringify(read_val));
73+
$rootScope.intro_done = read_val;
74+
});
75+
}
76+
7577
startprefs.isIntroDone = function() {
76-
var read_val = storage.get(INTRO_DONE_KEY);
77-
logger.log("in isIntroDone, read_val = "+read_val);
78-
if (read_val == null || read_val == "") {
78+
if ($rootScope.intro_done == null || $rootScope.intro_done == "") {
7979
logger.log("in isIntroDone, returning false");
80+
$rootScope.is_intro_done = false;
8081
return false;
8182
} else {
82-
logger.log("in isIntroDone, returning "+read_val);
83-
return read_val;
83+
logger.log("in isIntroDone, returning true");
84+
$rootScope.is_intro_done = true;
85+
return true;
8486
}
8587
}
8688

8789
startprefs.isConsented = function() {
88-
logger.log("curr_consented = "+$rootScope.curr_consented+
89-
"isIntroDone = " + startprefs.isIntroDone());
90-
if (startprefs.isIntroDone() &&
91-
($rootScope.curr_consented == null || $rootScope.curr_consented == "")) {
92-
alert("intro is done, but consent not found, re-consenting...");
93-
}
9490
if ($rootScope.curr_consented == null || $rootScope.curr_consented == "" ||
9591
$rootScope.curr_consented.approval_date != $rootScope.req_consent.approval_date) {
9692
console.log("Not consented in local storage, need to show consent");
93+
$rootScope.is_consented = false;
9794
return false;
9895
} else {
9996
console.log("Consented in local storage, no need to show consent");
97+
$rootScope.is_consented = true;
10098
return true;
10199
}
102100
}
103101

104102
startprefs.readConsentState = function() {
105-
/*
106-
* Read from local storage and move on so that we don't depend on native code.
107-
* Native code will be checked once the plugins are ready
108-
*/
109-
if (angular.isDefined($rootScope.req_consent) &&
110-
angular.isDefined($rootScope.curr_consented) &&
111-
$rootScope.curr_consented != null) {
112-
// consent state is all populated
113-
logger.log("req_consent = "+$rootScope.req_consent
114-
+" curr_consented = " + $rootScope.curr_consented);
115-
return new Promise(function(resolve, reject) {
116-
logger.log("resolving with empty information");
117-
resolve();
118-
});
119-
} else {
120-
// read consent state from the file and populate it
121-
return $http.get("json/startupConfig.json")
122-
.then(function(startupConfigResult) {
123-
$rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol;
124-
logger.log("required consent version = " + JSON.stringify($rootScope.req_consent));
125-
$rootScope.curr_consented = storage.get(
126-
DATA_COLLECTION_CONSENTED_PROTOCOL);
103+
// read consent state from the file and populate it
104+
return $http.get("json/startupConfig.json")
105+
.then(function(startupConfigResult) {
106+
$rootScope.req_consent = startupConfigResult.data.emSensorDataCollectionProtocol;
107+
logger.log("required consent version = " + JSON.stringify($rootScope.req_consent));
108+
return KVStore.get(DATA_COLLECTION_CONSENTED_PROTOCOL);
109+
}).then(function(kv_store_consent) {
110+
$rootScope.curr_consented = kv_store_consent;
111+
console.assert(angular.isDefined($rootScope.req_consent), "in readConsentState $rootScope.req_consent", JSON.stringify($rootScope.req_consent));
112+
// we can just launch this, we don't need to wait for it
113+
startprefs.checkNativeConsent();
127114
});
128-
}
129115
}
130116

131117
/*
@@ -135,26 +121,33 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger',
135121
*/
136122

137123
startprefs.getPendingOnboardingState = function() {
138-
if (!startprefs.isIntroDone()) {
139-
// Since we must return a promise when the intro is done,
140-
// we create and return one here even though we don't need it
141-
return new Promise(function(resolve, reject) {
142-
resolve('root.intro');
143-
});
144-
} else {
145-
// intro is done. Now, let's read and check the current version
146-
// of the startup config
147-
return $http.get("json/startupConfig.json")
148-
.then(startprefs.readConsentState)
149-
.then(startprefs.isConsented)
150-
.then(function(result) {
151-
if (result) {
152-
return null;
124+
return startprefs.readStartupState().then(function([is_intro_done, is_consented]) {
125+
if (!is_intro_done) {
126+
console.assert(!$rootScope.intro_done, "in getPendingOnboardingState first check, $rootScope.intro_done", JSON.stringify($rootScope.intro_done));
127+
return 'root.intro';
128+
} else {
129+
// intro is done. Now let's check consent
130+
console.assert(is_intro_done, "in getPendingOnboardingState, local is_intro_done", is_intro_done);
131+
console.assert($rootScope.is_intro_done, "in getPendingOnboardingState, $rootScope.intro_done", $rootScope.intro_done);
132+
if (is_consented) {
133+
return null;
153134
} else {
154-
return 'root.reconsent';
135+
return 'root.reconsent';
155136
}
156-
});
157-
}
137+
}
138+
});
139+
};
140+
141+
/*
142+
* Read the intro_done and consent_done variables into the $rootScope so that
143+
* we can use them without making multiple native calls
144+
*/
145+
startprefs.readStartupState = function() {
146+
var readIntroPromise = startprefs.readIntroDone()
147+
.then(startprefs.isIntroDone);
148+
var readConsentPromise = startprefs.readConsentState()
149+
.then(startprefs.isConsented);
150+
return Promise.all([readIntroPromise, readConsentPromise]);
158151
};
159152

160153
startprefs.getConsentDocument = function() {
@@ -171,61 +164,16 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger',
171164
startprefs.checkNativeConsent = function() {
172165
startprefs.getConsentDocument().then(function(resultDoc) {
173166
if (resultDoc == null) {
174-
startprefs.readConsentState()
175-
.then(startprefs.isConsented)
176-
.then(function(consentState) {
177-
if (consentState == true) {
178-
$ionicPopup.alert({template: "Local consent found, native consent missing, writing consent to native"});
179-
return writeConsentToNative();
180-
}
181-
});
182-
}
183-
});
184-
}
185-
186-
startprefs.checkUsercacheStorage = function(key) {
187-
// console.log("checkUsercacheStorage called");
188-
var ls_stored_val = storage.get(key);
189-
$window.cordova.plugins.BEMUserCache.getLocalStorage(key, false).then(function(uc_stored_val) {
190-
logger.log("uc_stored_val = "+JSON.stringify(uc_stored_val)+" ls_stored_val = "+ls_stored_val);
191-
if(angular.isDefined(uc_stored_val) && (uc_stored_val != null)
192-
&& (key in uc_stored_val) && angular.isDefined(uc_stored_val[key])) {
193-
if (ls_stored_val == true) {
194-
logger.log("local intro_done true, remote intro_done "+uc_stored_val[key]+", already synced");
167+
if(startprefs.isConsented()) {
168+
$ionicPopup.alert({template: "Local consent found, native consent missing, writing consent to native"});
169+
return writeConsentToNative();
195170
} else {
196-
logger.log("local intro_done false, remote intro_done "+uc_stored_val[key]+", setting local");
197-
$ionicPopup.alert({template: "Local "+key+" not found, native "+key+" found, writing "+key+" to local"})
198-
storage.put(key, true);
199-
}
200-
} else {
201-
if (ls_stored_val == true) {
202-
logger.log("local intro_done found, remote intro_done not found, setting remote");
203-
204-
// Need to initialize this first because if we try to
205-
// create it inlike with {key: value}, the key becomes the
206-
// word "key" and the stored object is
207-
// {"key":"2018-01-31T06:26:02+00:00"}
208-
var to_put = {};
209-
to_put[key] = moment().format();
210-
$window.cordova.plugins.BEMUserCache.putLocalStorage(key, to_put);
211-
$ionicPopup.alert({template: "Local "+key+" found, native "+key+" missing, writing "+key+" to native"})
212-
} else {
213-
logger.log("local intro_done false, remote intro_done not found, already synced");
171+
logger.log("Both local and native consent not found, nothing to sync");
214172
}
215173
}
216-
}).catch(function(error) {
217-
var display_msg = error.message + "\n" + error.stack;
218-
logger.log("error in checkUsercacheStorage = "+display_msg);
219-
$ionicPopup.alert({template: display_msg});
220174
});
221175
}
222176

223-
startprefs.checkStorageConsistency = function() {
224-
// console.log("checkStorageConsistency called");
225-
startprefs.checkNativeConsent();
226-
startprefs.checkUsercacheStorage(INTRO_DONE_KEY);
227-
}
228-
229177
startprefs.getNextState = function() {
230178
return startprefs.getPendingOnboardingState().then(function(result){
231179
if (result == null) {
@@ -295,7 +243,6 @@ angular.module('emission.splash.startprefs', ['emission.plugin.logger',
295243
Logger.log("ionicPlatform.ready() called " + nTimesCalled+" times!");
296244
nTimesCalled = nTimesCalled + 1;
297245
startprefs.startWithPrefs();
298-
startprefs.checkStorageConsistency();
299246
Logger.log("startprefs startup done");
300247
});
301248

0 commit comments

Comments
 (0)