mobile/android/base/sync/SyncConfiguration.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     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/. */
     5 package org.mozilla.gecko.sync;
     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;
    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;
    22 import android.content.SharedPreferences;
    23 import android.content.SharedPreferences.Editor;
    25 public class SyncConfiguration {
    27   public class EditorBranch implements Editor {
    29     private String prefix;
    30     private Editor editor;
    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     }
    40     public void apply() {
    41       // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding.
    42       this.editor.commit();
    43     }
    45     @Override
    46     public Editor clear() {
    47       this.editor = this.editor.clear();
    48       return this;
    49     }
    51     @Override
    52     public boolean commit() {
    53       return this.editor.commit();
    54     }
    56     @Override
    57     public Editor putBoolean(String key, boolean value) {
    58       this.editor = this.editor.putBoolean(prefix + key, value);
    59       return this;
    60     }
    62     @Override
    63     public Editor putFloat(String key, float value) {
    64       this.editor = this.editor.putFloat(prefix + key, value);
    65       return this;
    66     }
    68     @Override
    69     public Editor putInt(String key, int value) {
    70       this.editor = this.editor.putInt(prefix + key, value);
    71       return this;
    72     }
    74     @Override
    75     public Editor putLong(String key, long value) {
    76       this.editor = this.editor.putLong(prefix + key, value);
    77       return this;
    78     }
    80     @Override
    81     public Editor putString(String key, String value) {
    82       this.editor = this.editor.putString(prefix + key, value);
    83       return this;
    84     }
    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     }
    92     @Override
    93     public Editor remove(String key) {
    94       this.editor = this.editor.remove(prefix + key);
    95       return this;
    96     }
    98   }
   100   /**
   101    * A wrapper around a portion of the SharedPreferences space.
   102    *
   103    * @author rnewman
   104    *
   105    */
   106   public class ConfigurationBranch implements SharedPreferences {
   108     private SyncConfiguration config;
   109     private String prefix;                // Including trailing period.
   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     }
   120     @Override
   121     public boolean contains(String key) {
   122       return config.getPrefs().contains(prefix + key);
   123     }
   125     @Override
   126     public Editor edit() {
   127       return new EditorBranch(config, prefix);
   128     }
   130     @Override
   131     public Map<String, ?> getAll() {
   132       // Not implemented. TODO
   133       return null;
   134     }
   136     @Override
   137     public boolean getBoolean(String key, boolean defValue) {
   138       return config.getPrefs().getBoolean(prefix + key, defValue);
   139     }
   141     @Override
   142     public float getFloat(String key, float defValue) {
   143       return config.getPrefs().getFloat(prefix + key, defValue);
   144     }
   146     @Override
   147     public int getInt(String key, int defValue) {
   148       return config.getPrefs().getInt(prefix + key, defValue);
   149     }
   151     @Override
   152     public long getLong(String key, long defValue) {
   153       return config.getPrefs().getLong(prefix + key, defValue);
   154     }
   156     @Override
   157     public String getString(String key, String defValue) {
   158       return config.getPrefs().getString(prefix + key, defValue);
   159     }
   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     }
   167     @Override
   168     public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
   169       config.getPrefs().registerOnSharedPreferenceChangeListener(listener);
   170     }
   172     @Override
   173     public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
   174       config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener);
   175     }
   176   }
   178   private static final String LOG_TAG = "SyncConfiguration";
   180   // These must be set in GlobalSession's constructor.
   181   public URI             clusterURL;
   182   public KeyBundle       syncKeyBundle;
   184   public CollectionKeys  collectionKeys;
   185   public InfoCollections infoCollections;
   186   public MetaGlobal      metaGlobal;
   187   public String          syncID;
   189   protected final String username;
   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>();
   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;
   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;
   238   public SharedPreferences prefs;
   240   protected final AuthHeaderProvider authHeaderProvider;
   242   public static final String PREF_PREFS_VERSION = "prefs.version";
   243   public static final long CURRENT_PREFS_VERSION = 1;
   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.
   248   public static final String PREF_CLUSTER_URL = "clusterURL";
   249   public static final String PREF_SYNC_ID = "syncID";
   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";
   256   public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
   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";
   262   private static final String API_VERSION = "1.5";
   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   }
   271   public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) {
   272     this(username, authHeaderProvider, prefs);
   273     this.syncKeyBundle = syncKeyBundle;
   274   }
   276   public String getAPIVersion() {
   277     return API_VERSION;
   278   }
   280   public SharedPreferences getPrefs() {
   281     return this.prefs;
   282   }
   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   }
   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   }
   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   }
   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   }
   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   }
   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   }
   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     }
   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     }
   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   }
   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   }
   447   public void persistToPrefs() {
   448     this.persistToPrefs(this.getPrefs());
   449   }
   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   }
   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   }
   489   public AuthHeaderProvider getAuthHeaderProvider() {
   490     return authHeaderProvider;
   491   }
   493   public CollectionKeys getCollectionKeys() {
   494     return collectionKeys;
   495   }
   497   public void setCollectionKeys(CollectionKeys k) {
   498     collectionKeys = k;
   499   }
   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   }
   510   protected String infoBaseURL() {
   511     return clusterURL + "/info/";
   512   }
   514   public String infoCollectionsURL() {
   515     return infoBaseURL() + "collections";
   516   }
   518   public String infoCollectionCountsURL() {
   519     return infoBaseURL() + "collection_counts";
   520   }
   522   public String metaURL() {
   523     return storageURL() + "/meta/global";
   524   }
   526   public URI collectionURI(String collection) throws URISyntaxException {
   527     return new URI(storageURL() + "/" + collection);
   528   }
   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   }
   546   public URI wboURI(String collection, String id) throws URISyntaxException {
   547     return new URI(storageURL() + "/" + collection + "/" + id);
   548   }
   550   public URI keysURI() throws URISyntaxException {
   551     return wboURI("crypto", "keys");
   552   }
   554   public URI getClusterURL() {
   555     return clusterURL;
   556   }
   558   public String getClusterURLString() {
   559     if (clusterURL == null) {
   560       return null;
   561     }
   562     return clusterURL.toASCIIString();
   563   }
   565   public void setClusterURL(URI u) {
   566     this.clusterURL = u;
   567   }
   569   /**
   570    * Used for direct management of related prefs.
   571    */
   572   public Editor getEditor() {
   573     return this.getPrefs().edit();
   574   }
   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   }
   584   public long getPersistedServerClientRecordTimestamp() {
   585     return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0);
   586   }
   588   public void persistServerClientsTimestamp(long timestamp) {
   589     getEditor().putLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, timestamp).commit();
   590   }
   592   public long getPersistedServerClientsTimestamp() {
   593     return getPrefs().getLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, 0);
   594   }
   596   public void purgeCryptoKeys() {
   597     if (collectionKeys != null) {
   598       collectionKeys.clear();
   599     }
   600     persistedCryptoKeys().purge();
   601   }
   603   public void purgeMetaGlobal() {
   604     metaGlobal = null;
   605     persistedMetaGlobal().purge();
   606   }
   608   public PersistedCrypto5Keys persistedCryptoKeys() {
   609     return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle);
   610   }
   612   public PersistedMetaGlobal persistedMetaGlobal() {
   613     return new PersistedMetaGlobal(getPrefs());
   614   }
   615 }

mercurial