mobile/android/base/sync/SyncConfiguration.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:54d637d2cb1b
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 package org.mozilla.gecko.sync;
6
7 import java.net.URI;
8 import java.net.URISyntaxException;
9 import java.util.Collection;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.Map;
13 import java.util.Map.Entry;
14 import java.util.Set;
15
16 import org.mozilla.gecko.background.common.log.Logger;
17 import org.mozilla.gecko.sync.crypto.KeyBundle;
18 import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
19 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
20 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
21
22 import android.content.SharedPreferences;
23 import android.content.SharedPreferences.Editor;
24
25 public class SyncConfiguration {
26
27 public class EditorBranch implements Editor {
28
29 private String prefix;
30 private Editor editor;
31
32 public EditorBranch(SyncConfiguration config, String prefix) {
33 if (!prefix.endsWith(".")) {
34 throw new IllegalArgumentException("No trailing period in prefix.");
35 }
36 this.prefix = prefix;
37 this.editor = config.getEditor();
38 }
39
40 public void apply() {
41 // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding.
42 this.editor.commit();
43 }
44
45 @Override
46 public Editor clear() {
47 this.editor = this.editor.clear();
48 return this;
49 }
50
51 @Override
52 public boolean commit() {
53 return this.editor.commit();
54 }
55
56 @Override
57 public Editor putBoolean(String key, boolean value) {
58 this.editor = this.editor.putBoolean(prefix + key, value);
59 return this;
60 }
61
62 @Override
63 public Editor putFloat(String key, float value) {
64 this.editor = this.editor.putFloat(prefix + key, value);
65 return this;
66 }
67
68 @Override
69 public Editor putInt(String key, int value) {
70 this.editor = this.editor.putInt(prefix + key, value);
71 return this;
72 }
73
74 @Override
75 public Editor putLong(String key, long value) {
76 this.editor = this.editor.putLong(prefix + key, value);
77 return this;
78 }
79
80 @Override
81 public Editor putString(String key, String value) {
82 this.editor = this.editor.putString(prefix + key, value);
83 return this;
84 }
85
86 // Not marking as Override, because Android <= 10 doesn't have
87 // putStringSet. Neither can we implement it.
88 public Editor putStringSet(String key, Set<String> value) {
89 throw new RuntimeException("putStringSet not available.");
90 }
91
92 @Override
93 public Editor remove(String key) {
94 this.editor = this.editor.remove(prefix + key);
95 return this;
96 }
97
98 }
99
100 /**
101 * A wrapper around a portion of the SharedPreferences space.
102 *
103 * @author rnewman
104 *
105 */
106 public class ConfigurationBranch implements SharedPreferences {
107
108 private SyncConfiguration config;
109 private String prefix; // Including trailing period.
110
111 public ConfigurationBranch(SyncConfiguration syncConfiguration,
112 String prefix) {
113 if (!prefix.endsWith(".")) {
114 throw new IllegalArgumentException("No trailing period in prefix.");
115 }
116 this.config = syncConfiguration;
117 this.prefix = prefix;
118 }
119
120 @Override
121 public boolean contains(String key) {
122 return config.getPrefs().contains(prefix + key);
123 }
124
125 @Override
126 public Editor edit() {
127 return new EditorBranch(config, prefix);
128 }
129
130 @Override
131 public Map<String, ?> getAll() {
132 // Not implemented. TODO
133 return null;
134 }
135
136 @Override
137 public boolean getBoolean(String key, boolean defValue) {
138 return config.getPrefs().getBoolean(prefix + key, defValue);
139 }
140
141 @Override
142 public float getFloat(String key, float defValue) {
143 return config.getPrefs().getFloat(prefix + key, defValue);
144 }
145
146 @Override
147 public int getInt(String key, int defValue) {
148 return config.getPrefs().getInt(prefix + key, defValue);
149 }
150
151 @Override
152 public long getLong(String key, long defValue) {
153 return config.getPrefs().getLong(prefix + key, defValue);
154 }
155
156 @Override
157 public String getString(String key, String defValue) {
158 return config.getPrefs().getString(prefix + key, defValue);
159 }
160
161 // Not marking as Override, because Android <= 10 doesn't have
162 // getStringSet. Neither can we implement it.
163 public Set<String> getStringSet(String key, Set<String> defValue) {
164 throw new RuntimeException("getStringSet not available.");
165 }
166
167 @Override
168 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
169 config.getPrefs().registerOnSharedPreferenceChangeListener(listener);
170 }
171
172 @Override
173 public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
174 config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener);
175 }
176 }
177
178 private static final String LOG_TAG = "SyncConfiguration";
179
180 // These must be set in GlobalSession's constructor.
181 public URI clusterURL;
182 public KeyBundle syncKeyBundle;
183
184 public CollectionKeys collectionKeys;
185 public InfoCollections infoCollections;
186 public MetaGlobal metaGlobal;
187 public String syncID;
188
189 protected final String username;
190
191 /**
192 * Persisted collection of enabledEngineNames.
193 * <p>
194 * Can contain engines Android Sync is not currently aware of, such as "prefs"
195 * or "addons".
196 * <p>
197 * Copied from latest downloaded meta/global record and used to generate a
198 * fresh meta/global record for upload.
199 */
200 public Set<String> enabledEngineNames;
201 public Set<String> declinedEngineNames = new HashSet<String>();
202
203 /**
204 * Names of stages to sync <it>this sync</it>, or <code>null</code> to sync
205 * all known stages.
206 * <p>
207 * Generated <it>each sync</it> from extras bundle passed to
208 * <code>SyncAdapter.onPerformSync</code> and not persisted.
209 * <p>
210 * Not synchronized! Set this exactly once per global session and don't modify
211 * it -- especially not from multiple threads.
212 */
213 public Collection<String> stagesToSync;
214
215 /**
216 * Engines whose sync state has been modified by the user through
217 * SelectEnginesActivity, where each key-value pair is an engine name and
218 * its sync state.
219 *
220 * This differs from <code>enabledEngineNames</code> in that
221 * <code>enabledEngineNames</code> reflects the downloaded meta/global,
222 * whereas <code>userSelectedEngines</code> stores the differences in engines to
223 * sync that the user has selected.
224 *
225 * Each engine stage will check for engine changes at the beginning of the
226 * stage.
227 *
228 * If no engine sync state changes have been made by the user, userSelectedEngines
229 * will be null, and Sync will proceed normally.
230 *
231 * If the user has made changes to engine syncing state, each engine will sync
232 * according to the sync state specified in userSelectedEngines and propagate that
233 * state to meta/global, to be uploaded.
234 */
235 public Map<String, Boolean> userSelectedEngines;
236 public long userSelectedEnginesTimestamp;
237
238 public SharedPreferences prefs;
239
240 protected final AuthHeaderProvider authHeaderProvider;
241
242 public static final String PREF_PREFS_VERSION = "prefs.version";
243 public static final long CURRENT_PREFS_VERSION = 1;
244
245 public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp"; // When the collection was touched.
246 public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp"; // When our record was touched.
247
248 public static final String PREF_CLUSTER_URL = "clusterURL";
249 public static final String PREF_SYNC_ID = "syncID";
250
251 public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames";
252 public static final String PREF_DECLINED_ENGINE_NAMES = "declinedEngineNames";
253 public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC = "userSelectedEngines";
254 public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP = "userSelectedEnginesTimestamp";
255
256 public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
257
258 public static final String PREF_ACCOUNT_GUID = "account.guid";
259 public static final String PREF_CLIENT_NAME = "account.clientName";
260 public static final String PREF_NUM_CLIENTS = "account.numClients";
261
262 private static final String API_VERSION = "1.5";
263
264 public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs) {
265 this.username = username;
266 this.authHeaderProvider = authHeaderProvider;
267 this.prefs = prefs;
268 this.loadFromPrefs(prefs);
269 }
270
271 public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) {
272 this(username, authHeaderProvider, prefs);
273 this.syncKeyBundle = syncKeyBundle;
274 }
275
276 public String getAPIVersion() {
277 return API_VERSION;
278 }
279
280 public SharedPreferences getPrefs() {
281 return this.prefs;
282 }
283
284 /**
285 * Valid engines supported by Android Sync.
286 *
287 * @return Set<String> of valid engine names that Android Sync implements.
288 */
289 public static Set<String> validEngineNames() {
290 Set<String> engineNames = new HashSet<String>();
291 for (Stage stage : Stage.getNamedStages()) {
292 engineNames.add(stage.getRepositoryName());
293 }
294 return engineNames;
295 }
296
297 /**
298 * Return a convenient accessor for part of prefs.
299 * @return
300 * A ConfigurationBranch object representing this
301 * section of the preferences space.
302 */
303 public ConfigurationBranch getBranch(String prefix) {
304 return new ConfigurationBranch(this, prefix);
305 }
306
307 /**
308 * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global.
309 *
310 * @param prefs
311 * SharedPreferences that the engines are associated with.
312 * @param pref
313 * The preference name to use. E.g, PREF_ENABLED_ENGINE_NAMES.
314 * @return Set<String> of the enabled engine names if they have been stored,
315 * or null otherwise.
316 */
317 protected static Set<String> getEngineNamesFromPref(SharedPreferences prefs, String pref) {
318 final String json = prefs.getString(pref, null);
319 if (json == null) {
320 return null;
321 }
322 try {
323 final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
324 return new HashSet<String>(o.keySet());
325 } catch (Exception e) {
326 return null;
327 }
328 }
329
330 /**
331 * Returns the set of engine names that the user has enabled. If none
332 * have been stored in prefs, <code>null</code> is returned.
333 */
334 public static Set<String> getEnabledEngineNames(SharedPreferences prefs) {
335 return getEngineNamesFromPref(prefs, PREF_ENABLED_ENGINE_NAMES);
336 }
337
338 /**
339 * Returns the set of engine names that the user has declined.
340 */
341 public static Set<String> getDeclinedEngineNames(SharedPreferences prefs) {
342 final Set<String> names = getEngineNamesFromPref(prefs, PREF_DECLINED_ENGINE_NAMES);
343 if (names == null) {
344 return new HashSet<String>();
345 }
346 return names;
347 }
348
349 /**
350 * Gets the engines whose sync states have been changed by the user through the
351 * SelectEnginesActivity.
352 *
353 * @param prefs
354 * SharedPreferences of account that the engines are associated with.
355 * @return Map<String, Boolean> of changed engines. Key is the lower-cased
356 * engine name, Value is the new sync state.
357 */
358 public static Map<String, Boolean> getUserSelectedEngines(SharedPreferences prefs) {
359 String json = prefs.getString(PREF_USER_SELECTED_ENGINES_TO_SYNC, null);
360 if (json == null) {
361 return null;
362 }
363 try {
364 ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
365 Map<String, Boolean> map = new HashMap<String, Boolean>();
366 for (Entry<String, Object> e : o.entrySet()) {
367 String key = e.getKey();
368 Boolean value = (Boolean) e.getValue();
369 map.put(key, value);
370 // Forms depends on history. Add forms if history is selected.
371 if ("history".equals(key)) {
372 map.put("forms", value);
373 }
374 }
375 // Sanity check: remove forms if history does not exist.
376 if (!map.containsKey("history")) {
377 map.remove("forms");
378 }
379 return map;
380 } catch (Exception e) {
381 return null;
382 }
383 }
384
385 /**
386 * Store a Map of engines and their sync states to prefs.
387 *
388 * Any engine that's disabled in the input is also recorded
389 * as a declined engine, overwriting the stored values.
390 *
391 * @param prefs
392 * SharedPreferences that the engines are associated with.
393 * @param selectedEngines
394 * Map<String, Boolean> of engine name to sync state
395 */
396 public static void storeSelectedEnginesToPrefs(SharedPreferences prefs, Map<String, Boolean> selectedEngines) {
397 ExtendedJSONObject jObj = new ExtendedJSONObject();
398 HashSet<String> declined = new HashSet<String>();
399 for (Entry<String, Boolean> e : selectedEngines.entrySet()) {
400 final Boolean enabled = e.getValue();
401 final String engine = e.getKey();
402 jObj.put(engine, enabled);
403 if (!enabled) {
404 declined.add(engine);
405 }
406 }
407
408 // Our history checkbox drives form history, too.
409 // We don't need to do this for enablement: that's done at retrieval time.
410 if (selectedEngines.containsKey("history") && !selectedEngines.get("history").booleanValue()) {
411 declined.add("forms");
412 }
413
414 String json = jObj.toJSONString();
415 long currentTime = System.currentTimeMillis();
416 Editor edit = prefs.edit();
417 edit.putString(PREF_USER_SELECTED_ENGINES_TO_SYNC, json);
418 edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declined));
419 edit.putLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, currentTime);
420 Logger.error(LOG_TAG, "Storing user-selected engines at [" + currentTime + "].");
421 edit.commit();
422 }
423
424 public void loadFromPrefs(SharedPreferences prefs) {
425 if (prefs.contains(PREF_CLUSTER_URL)) {
426 String u = prefs.getString(PREF_CLUSTER_URL, null);
427 try {
428 clusterURL = new URI(u);
429 Logger.trace(LOG_TAG, "Set clusterURL from bundle: " + u);
430 } catch (URISyntaxException e) {
431 Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e);
432 }
433 }
434 if (prefs.contains(PREF_SYNC_ID)) {
435 syncID = prefs.getString(PREF_SYNC_ID, null);
436 Logger.trace(LOG_TAG, "Set syncID from bundle: " + syncID);
437 }
438 enabledEngineNames = getEnabledEngineNames(prefs);
439 declinedEngineNames = getDeclinedEngineNames(prefs);
440 userSelectedEngines = getUserSelectedEngines(prefs);
441 userSelectedEnginesTimestamp = prefs.getLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, 0);
442 // We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
443 // and we won't have it on construction.
444 // TODO: MetaGlobal, password, infoCollections.
445 }
446
447 public void persistToPrefs() {
448 this.persistToPrefs(this.getPrefs());
449 }
450
451 private static String setToJSONObjectString(Set<String> set) {
452 ExtendedJSONObject o = new ExtendedJSONObject();
453 for (String name : set) {
454 o.put(name, 0);
455 }
456 return o.toJSONString();
457 }
458
459 public void persistToPrefs(SharedPreferences prefs) {
460 Editor edit = prefs.edit();
461 if (clusterURL == null) {
462 edit.remove(PREF_CLUSTER_URL);
463 } else {
464 edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString());
465 }
466 if (syncID != null) {
467 edit.putString(PREF_SYNC_ID, syncID);
468 }
469 if (enabledEngineNames == null) {
470 edit.remove(PREF_ENABLED_ENGINE_NAMES);
471 } else {
472 edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames));
473 }
474 if (declinedEngineNames.isEmpty()) {
475 edit.remove(PREF_DECLINED_ENGINE_NAMES);
476 } else {
477 edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames));
478 }
479 if (userSelectedEngines == null) {
480 edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC);
481 edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP);
482 }
483 // Don't bother saving userSelectedEngines - these should only be changed by
484 // SelectEnginesActivity.
485 edit.commit();
486 // TODO: keys.
487 }
488
489 public AuthHeaderProvider getAuthHeaderProvider() {
490 return authHeaderProvider;
491 }
492
493 public CollectionKeys getCollectionKeys() {
494 return collectionKeys;
495 }
496
497 public void setCollectionKeys(CollectionKeys k) {
498 collectionKeys = k;
499 }
500
501 /**
502 * Return path to storage endpoint without trailing slash.
503 *
504 * @return storage endpoint without trailing slash.
505 */
506 public String storageURL() {
507 return clusterURL + "/storage";
508 }
509
510 protected String infoBaseURL() {
511 return clusterURL + "/info/";
512 }
513
514 public String infoCollectionsURL() {
515 return infoBaseURL() + "collections";
516 }
517
518 public String infoCollectionCountsURL() {
519 return infoBaseURL() + "collection_counts";
520 }
521
522 public String metaURL() {
523 return storageURL() + "/meta/global";
524 }
525
526 public URI collectionURI(String collection) throws URISyntaxException {
527 return new URI(storageURL() + "/" + collection);
528 }
529
530 public URI collectionURI(String collection, boolean full) throws URISyntaxException {
531 // Do it this way to make it easier to add more params later.
532 // It's pretty ugly, I'll grant.
533 boolean anyParams = full;
534 String uriParams = "";
535 if (anyParams) {
536 StringBuilder params = new StringBuilder("?");
537 if (full) {
538 params.append("full=1");
539 }
540 uriParams = params.toString();
541 }
542 String uri = storageURL() + "/" + collection + uriParams;
543 return new URI(uri);
544 }
545
546 public URI wboURI(String collection, String id) throws URISyntaxException {
547 return new URI(storageURL() + "/" + collection + "/" + id);
548 }
549
550 public URI keysURI() throws URISyntaxException {
551 return wboURI("crypto", "keys");
552 }
553
554 public URI getClusterURL() {
555 return clusterURL;
556 }
557
558 public String getClusterURLString() {
559 if (clusterURL == null) {
560 return null;
561 }
562 return clusterURL.toASCIIString();
563 }
564
565 public void setClusterURL(URI u) {
566 this.clusterURL = u;
567 }
568
569 /**
570 * Used for direct management of related prefs.
571 */
572 public Editor getEditor() {
573 return this.getPrefs().edit();
574 }
575
576 /**
577 * We persist two different clients timestamps: our own record's,
578 * and the timestamp for the collection.
579 */
580 public void persistServerClientRecordTimestamp(long timestamp) {
581 getEditor().putLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, timestamp).commit();
582 }
583
584 public long getPersistedServerClientRecordTimestamp() {
585 return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0);
586 }
587
588 public void persistServerClientsTimestamp(long timestamp) {
589 getEditor().putLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, timestamp).commit();
590 }
591
592 public long getPersistedServerClientsTimestamp() {
593 return getPrefs().getLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, 0);
594 }
595
596 public void purgeCryptoKeys() {
597 if (collectionKeys != null) {
598 collectionKeys.clear();
599 }
600 persistedCryptoKeys().purge();
601 }
602
603 public void purgeMetaGlobal() {
604 metaGlobal = null;
605 persistedMetaGlobal().purge();
606 }
607
608 public PersistedCrypto5Keys persistedCryptoKeys() {
609 return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle);
610 }
611
612 public PersistedMetaGlobal persistedMetaGlobal() {
613 return new PersistedMetaGlobal(getPrefs());
614 }
615 }

mercurial