michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync; michael@0: michael@0: import java.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.util.Collection; michael@0: import java.util.HashMap; michael@0: import java.util.HashSet; michael@0: import java.util.Map; michael@0: import java.util.Map.Entry; michael@0: import java.util.Set; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.sync.crypto.KeyBundle; michael@0: import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys; michael@0: import org.mozilla.gecko.sync.net.AuthHeaderProvider; michael@0: import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; michael@0: michael@0: import android.content.SharedPreferences; michael@0: import android.content.SharedPreferences.Editor; michael@0: michael@0: public class SyncConfiguration { michael@0: michael@0: public class EditorBranch implements Editor { michael@0: michael@0: private String prefix; michael@0: private Editor editor; michael@0: michael@0: public EditorBranch(SyncConfiguration config, String prefix) { michael@0: if (!prefix.endsWith(".")) { michael@0: throw new IllegalArgumentException("No trailing period in prefix."); michael@0: } michael@0: this.prefix = prefix; michael@0: this.editor = config.getEditor(); michael@0: } michael@0: michael@0: public void apply() { michael@0: // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding. michael@0: this.editor.commit(); michael@0: } michael@0: michael@0: @Override michael@0: public Editor clear() { michael@0: this.editor = this.editor.clear(); michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public boolean commit() { michael@0: return this.editor.commit(); michael@0: } michael@0: michael@0: @Override michael@0: public Editor putBoolean(String key, boolean value) { michael@0: this.editor = this.editor.putBoolean(prefix + key, value); michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public Editor putFloat(String key, float value) { michael@0: this.editor = this.editor.putFloat(prefix + key, value); michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public Editor putInt(String key, int value) { michael@0: this.editor = this.editor.putInt(prefix + key, value); michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public Editor putLong(String key, long value) { michael@0: this.editor = this.editor.putLong(prefix + key, value); michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public Editor putString(String key, String value) { michael@0: this.editor = this.editor.putString(prefix + key, value); michael@0: return this; michael@0: } michael@0: michael@0: // Not marking as Override, because Android <= 10 doesn't have michael@0: // putStringSet. Neither can we implement it. michael@0: public Editor putStringSet(String key, Set value) { michael@0: throw new RuntimeException("putStringSet not available."); michael@0: } michael@0: michael@0: @Override michael@0: public Editor remove(String key) { michael@0: this.editor = this.editor.remove(prefix + key); michael@0: return this; michael@0: } michael@0: michael@0: } michael@0: michael@0: /** michael@0: * A wrapper around a portion of the SharedPreferences space. michael@0: * michael@0: * @author rnewman michael@0: * michael@0: */ michael@0: public class ConfigurationBranch implements SharedPreferences { michael@0: michael@0: private SyncConfiguration config; michael@0: private String prefix; // Including trailing period. michael@0: michael@0: public ConfigurationBranch(SyncConfiguration syncConfiguration, michael@0: String prefix) { michael@0: if (!prefix.endsWith(".")) { michael@0: throw new IllegalArgumentException("No trailing period in prefix."); michael@0: } michael@0: this.config = syncConfiguration; michael@0: this.prefix = prefix; michael@0: } michael@0: michael@0: @Override michael@0: public boolean contains(String key) { michael@0: return config.getPrefs().contains(prefix + key); michael@0: } michael@0: michael@0: @Override michael@0: public Editor edit() { michael@0: return new EditorBranch(config, prefix); michael@0: } michael@0: michael@0: @Override michael@0: public Map getAll() { michael@0: // Not implemented. TODO michael@0: return null; michael@0: } michael@0: michael@0: @Override michael@0: public boolean getBoolean(String key, boolean defValue) { michael@0: return config.getPrefs().getBoolean(prefix + key, defValue); michael@0: } michael@0: michael@0: @Override michael@0: public float getFloat(String key, float defValue) { michael@0: return config.getPrefs().getFloat(prefix + key, defValue); michael@0: } michael@0: michael@0: @Override michael@0: public int getInt(String key, int defValue) { michael@0: return config.getPrefs().getInt(prefix + key, defValue); michael@0: } michael@0: michael@0: @Override michael@0: public long getLong(String key, long defValue) { michael@0: return config.getPrefs().getLong(prefix + key, defValue); michael@0: } michael@0: michael@0: @Override michael@0: public String getString(String key, String defValue) { michael@0: return config.getPrefs().getString(prefix + key, defValue); michael@0: } michael@0: michael@0: // Not marking as Override, because Android <= 10 doesn't have michael@0: // getStringSet. Neither can we implement it. michael@0: public Set getStringSet(String key, Set defValue) { michael@0: throw new RuntimeException("getStringSet not available."); michael@0: } michael@0: michael@0: @Override michael@0: public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { michael@0: config.getPrefs().registerOnSharedPreferenceChangeListener(listener); michael@0: } michael@0: michael@0: @Override michael@0: public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { michael@0: config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener); michael@0: } michael@0: } michael@0: michael@0: private static final String LOG_TAG = "SyncConfiguration"; michael@0: michael@0: // These must be set in GlobalSession's constructor. michael@0: public URI clusterURL; michael@0: public KeyBundle syncKeyBundle; michael@0: michael@0: public CollectionKeys collectionKeys; michael@0: public InfoCollections infoCollections; michael@0: public MetaGlobal metaGlobal; michael@0: public String syncID; michael@0: michael@0: protected final String username; michael@0: michael@0: /** michael@0: * Persisted collection of enabledEngineNames. michael@0: *

michael@0: * Can contain engines Android Sync is not currently aware of, such as "prefs" michael@0: * or "addons". michael@0: *

michael@0: * Copied from latest downloaded meta/global record and used to generate a michael@0: * fresh meta/global record for upload. michael@0: */ michael@0: public Set enabledEngineNames; michael@0: public Set declinedEngineNames = new HashSet(); michael@0: michael@0: /** michael@0: * Names of stages to sync this sync, or null to sync michael@0: * all known stages. michael@0: *

michael@0: * Generated each sync from extras bundle passed to michael@0: * SyncAdapter.onPerformSync and not persisted. michael@0: *

michael@0: * Not synchronized! Set this exactly once per global session and don't modify michael@0: * it -- especially not from multiple threads. michael@0: */ michael@0: public Collection stagesToSync; michael@0: michael@0: /** michael@0: * Engines whose sync state has been modified by the user through michael@0: * SelectEnginesActivity, where each key-value pair is an engine name and michael@0: * its sync state. michael@0: * michael@0: * This differs from enabledEngineNames in that michael@0: * enabledEngineNames reflects the downloaded meta/global, michael@0: * whereas userSelectedEngines stores the differences in engines to michael@0: * sync that the user has selected. michael@0: * michael@0: * Each engine stage will check for engine changes at the beginning of the michael@0: * stage. michael@0: * michael@0: * If no engine sync state changes have been made by the user, userSelectedEngines michael@0: * will be null, and Sync will proceed normally. michael@0: * michael@0: * If the user has made changes to engine syncing state, each engine will sync michael@0: * according to the sync state specified in userSelectedEngines and propagate that michael@0: * state to meta/global, to be uploaded. michael@0: */ michael@0: public Map userSelectedEngines; michael@0: public long userSelectedEnginesTimestamp; michael@0: michael@0: public SharedPreferences prefs; michael@0: michael@0: protected final AuthHeaderProvider authHeaderProvider; michael@0: michael@0: public static final String PREF_PREFS_VERSION = "prefs.version"; michael@0: public static final long CURRENT_PREFS_VERSION = 1; michael@0: michael@0: public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp"; // When the collection was touched. michael@0: public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp"; // When our record was touched. michael@0: michael@0: public static final String PREF_CLUSTER_URL = "clusterURL"; michael@0: public static final String PREF_SYNC_ID = "syncID"; michael@0: michael@0: public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames"; michael@0: public static final String PREF_DECLINED_ENGINE_NAMES = "declinedEngineNames"; michael@0: public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC = "userSelectedEngines"; michael@0: public static final String PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP = "userSelectedEnginesTimestamp"; michael@0: michael@0: public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale"; michael@0: michael@0: public static final String PREF_ACCOUNT_GUID = "account.guid"; michael@0: public static final String PREF_CLIENT_NAME = "account.clientName"; michael@0: public static final String PREF_NUM_CLIENTS = "account.numClients"; michael@0: michael@0: private static final String API_VERSION = "1.5"; michael@0: michael@0: public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs) { michael@0: this.username = username; michael@0: this.authHeaderProvider = authHeaderProvider; michael@0: this.prefs = prefs; michael@0: this.loadFromPrefs(prefs); michael@0: } michael@0: michael@0: public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) { michael@0: this(username, authHeaderProvider, prefs); michael@0: this.syncKeyBundle = syncKeyBundle; michael@0: } michael@0: michael@0: public String getAPIVersion() { michael@0: return API_VERSION; michael@0: } michael@0: michael@0: public SharedPreferences getPrefs() { michael@0: return this.prefs; michael@0: } michael@0: michael@0: /** michael@0: * Valid engines supported by Android Sync. michael@0: * michael@0: * @return Set of valid engine names that Android Sync implements. michael@0: */ michael@0: public static Set validEngineNames() { michael@0: Set engineNames = new HashSet(); michael@0: for (Stage stage : Stage.getNamedStages()) { michael@0: engineNames.add(stage.getRepositoryName()); michael@0: } michael@0: return engineNames; michael@0: } michael@0: michael@0: /** michael@0: * Return a convenient accessor for part of prefs. michael@0: * @return michael@0: * A ConfigurationBranch object representing this michael@0: * section of the preferences space. michael@0: */ michael@0: public ConfigurationBranch getBranch(String prefix) { michael@0: return new ConfigurationBranch(this, prefix); michael@0: } michael@0: michael@0: /** michael@0: * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global. michael@0: * michael@0: * @param prefs michael@0: * SharedPreferences that the engines are associated with. michael@0: * @param pref michael@0: * The preference name to use. E.g, PREF_ENABLED_ENGINE_NAMES. michael@0: * @return Set of the enabled engine names if they have been stored, michael@0: * or null otherwise. michael@0: */ michael@0: protected static Set getEngineNamesFromPref(SharedPreferences prefs, String pref) { michael@0: final String json = prefs.getString(pref, null); michael@0: if (json == null) { michael@0: return null; michael@0: } michael@0: try { michael@0: final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json); michael@0: return new HashSet(o.keySet()); michael@0: } catch (Exception e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the set of engine names that the user has enabled. If none michael@0: * have been stored in prefs, null is returned. michael@0: */ michael@0: public static Set getEnabledEngineNames(SharedPreferences prefs) { michael@0: return getEngineNamesFromPref(prefs, PREF_ENABLED_ENGINE_NAMES); michael@0: } michael@0: michael@0: /** michael@0: * Returns the set of engine names that the user has declined. michael@0: */ michael@0: public static Set getDeclinedEngineNames(SharedPreferences prefs) { michael@0: final Set names = getEngineNamesFromPref(prefs, PREF_DECLINED_ENGINE_NAMES); michael@0: if (names == null) { michael@0: return new HashSet(); michael@0: } michael@0: return names; michael@0: } michael@0: michael@0: /** michael@0: * Gets the engines whose sync states have been changed by the user through the michael@0: * SelectEnginesActivity. michael@0: * michael@0: * @param prefs michael@0: * SharedPreferences of account that the engines are associated with. michael@0: * @return Map of changed engines. Key is the lower-cased michael@0: * engine name, Value is the new sync state. michael@0: */ michael@0: public static Map getUserSelectedEngines(SharedPreferences prefs) { michael@0: String json = prefs.getString(PREF_USER_SELECTED_ENGINES_TO_SYNC, null); michael@0: if (json == null) { michael@0: return null; michael@0: } michael@0: try { michael@0: ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json); michael@0: Map map = new HashMap(); michael@0: for (Entry e : o.entrySet()) { michael@0: String key = e.getKey(); michael@0: Boolean value = (Boolean) e.getValue(); michael@0: map.put(key, value); michael@0: // Forms depends on history. Add forms if history is selected. michael@0: if ("history".equals(key)) { michael@0: map.put("forms", value); michael@0: } michael@0: } michael@0: // Sanity check: remove forms if history does not exist. michael@0: if (!map.containsKey("history")) { michael@0: map.remove("forms"); michael@0: } michael@0: return map; michael@0: } catch (Exception e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Store a Map of engines and their sync states to prefs. michael@0: * michael@0: * Any engine that's disabled in the input is also recorded michael@0: * as a declined engine, overwriting the stored values. michael@0: * michael@0: * @param prefs michael@0: * SharedPreferences that the engines are associated with. michael@0: * @param selectedEngines michael@0: * Map of engine name to sync state michael@0: */ michael@0: public static void storeSelectedEnginesToPrefs(SharedPreferences prefs, Map selectedEngines) { michael@0: ExtendedJSONObject jObj = new ExtendedJSONObject(); michael@0: HashSet declined = new HashSet(); michael@0: for (Entry e : selectedEngines.entrySet()) { michael@0: final Boolean enabled = e.getValue(); michael@0: final String engine = e.getKey(); michael@0: jObj.put(engine, enabled); michael@0: if (!enabled) { michael@0: declined.add(engine); michael@0: } michael@0: } michael@0: michael@0: // Our history checkbox drives form history, too. michael@0: // We don't need to do this for enablement: that's done at retrieval time. michael@0: if (selectedEngines.containsKey("history") && !selectedEngines.get("history").booleanValue()) { michael@0: declined.add("forms"); michael@0: } michael@0: michael@0: String json = jObj.toJSONString(); michael@0: long currentTime = System.currentTimeMillis(); michael@0: Editor edit = prefs.edit(); michael@0: edit.putString(PREF_USER_SELECTED_ENGINES_TO_SYNC, json); michael@0: edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declined)); michael@0: edit.putLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, currentTime); michael@0: Logger.error(LOG_TAG, "Storing user-selected engines at [" + currentTime + "]."); michael@0: edit.commit(); michael@0: } michael@0: michael@0: public void loadFromPrefs(SharedPreferences prefs) { michael@0: if (prefs.contains(PREF_CLUSTER_URL)) { michael@0: String u = prefs.getString(PREF_CLUSTER_URL, null); michael@0: try { michael@0: clusterURL = new URI(u); michael@0: Logger.trace(LOG_TAG, "Set clusterURL from bundle: " + u); michael@0: } catch (URISyntaxException e) { michael@0: Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e); michael@0: } michael@0: } michael@0: if (prefs.contains(PREF_SYNC_ID)) { michael@0: syncID = prefs.getString(PREF_SYNC_ID, null); michael@0: Logger.trace(LOG_TAG, "Set syncID from bundle: " + syncID); michael@0: } michael@0: enabledEngineNames = getEnabledEngineNames(prefs); michael@0: declinedEngineNames = getDeclinedEngineNames(prefs); michael@0: userSelectedEngines = getUserSelectedEngines(prefs); michael@0: userSelectedEnginesTimestamp = prefs.getLong(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP, 0); michael@0: // We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON michael@0: // and we won't have it on construction. michael@0: // TODO: MetaGlobal, password, infoCollections. michael@0: } michael@0: michael@0: public void persistToPrefs() { michael@0: this.persistToPrefs(this.getPrefs()); michael@0: } michael@0: michael@0: private static String setToJSONObjectString(Set set) { michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: for (String name : set) { michael@0: o.put(name, 0); michael@0: } michael@0: return o.toJSONString(); michael@0: } michael@0: michael@0: public void persistToPrefs(SharedPreferences prefs) { michael@0: Editor edit = prefs.edit(); michael@0: if (clusterURL == null) { michael@0: edit.remove(PREF_CLUSTER_URL); michael@0: } else { michael@0: edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString()); michael@0: } michael@0: if (syncID != null) { michael@0: edit.putString(PREF_SYNC_ID, syncID); michael@0: } michael@0: if (enabledEngineNames == null) { michael@0: edit.remove(PREF_ENABLED_ENGINE_NAMES); michael@0: } else { michael@0: edit.putString(PREF_ENABLED_ENGINE_NAMES, setToJSONObjectString(enabledEngineNames)); michael@0: } michael@0: if (declinedEngineNames.isEmpty()) { michael@0: edit.remove(PREF_DECLINED_ENGINE_NAMES); michael@0: } else { michael@0: edit.putString(PREF_DECLINED_ENGINE_NAMES, setToJSONObjectString(declinedEngineNames)); michael@0: } michael@0: if (userSelectedEngines == null) { michael@0: edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC); michael@0: edit.remove(PREF_USER_SELECTED_ENGINES_TO_SYNC_TIMESTAMP); michael@0: } michael@0: // Don't bother saving userSelectedEngines - these should only be changed by michael@0: // SelectEnginesActivity. michael@0: edit.commit(); michael@0: // TODO: keys. michael@0: } michael@0: michael@0: public AuthHeaderProvider getAuthHeaderProvider() { michael@0: return authHeaderProvider; michael@0: } michael@0: michael@0: public CollectionKeys getCollectionKeys() { michael@0: return collectionKeys; michael@0: } michael@0: michael@0: public void setCollectionKeys(CollectionKeys k) { michael@0: collectionKeys = k; michael@0: } michael@0: michael@0: /** michael@0: * Return path to storage endpoint without trailing slash. michael@0: * michael@0: * @return storage endpoint without trailing slash. michael@0: */ michael@0: public String storageURL() { michael@0: return clusterURL + "/storage"; michael@0: } michael@0: michael@0: protected String infoBaseURL() { michael@0: return clusterURL + "/info/"; michael@0: } michael@0: michael@0: public String infoCollectionsURL() { michael@0: return infoBaseURL() + "collections"; michael@0: } michael@0: michael@0: public String infoCollectionCountsURL() { michael@0: return infoBaseURL() + "collection_counts"; michael@0: } michael@0: michael@0: public String metaURL() { michael@0: return storageURL() + "/meta/global"; michael@0: } michael@0: michael@0: public URI collectionURI(String collection) throws URISyntaxException { michael@0: return new URI(storageURL() + "/" + collection); michael@0: } michael@0: michael@0: public URI collectionURI(String collection, boolean full) throws URISyntaxException { michael@0: // Do it this way to make it easier to add more params later. michael@0: // It's pretty ugly, I'll grant. michael@0: boolean anyParams = full; michael@0: String uriParams = ""; michael@0: if (anyParams) { michael@0: StringBuilder params = new StringBuilder("?"); michael@0: if (full) { michael@0: params.append("full=1"); michael@0: } michael@0: uriParams = params.toString(); michael@0: } michael@0: String uri = storageURL() + "/" + collection + uriParams; michael@0: return new URI(uri); michael@0: } michael@0: michael@0: public URI wboURI(String collection, String id) throws URISyntaxException { michael@0: return new URI(storageURL() + "/" + collection + "/" + id); michael@0: } michael@0: michael@0: public URI keysURI() throws URISyntaxException { michael@0: return wboURI("crypto", "keys"); michael@0: } michael@0: michael@0: public URI getClusterURL() { michael@0: return clusterURL; michael@0: } michael@0: michael@0: public String getClusterURLString() { michael@0: if (clusterURL == null) { michael@0: return null; michael@0: } michael@0: return clusterURL.toASCIIString(); michael@0: } michael@0: michael@0: public void setClusterURL(URI u) { michael@0: this.clusterURL = u; michael@0: } michael@0: michael@0: /** michael@0: * Used for direct management of related prefs. michael@0: */ michael@0: public Editor getEditor() { michael@0: return this.getPrefs().edit(); michael@0: } michael@0: michael@0: /** michael@0: * We persist two different clients timestamps: our own record's, michael@0: * and the timestamp for the collection. michael@0: */ michael@0: public void persistServerClientRecordTimestamp(long timestamp) { michael@0: getEditor().putLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, timestamp).commit(); michael@0: } michael@0: michael@0: public long getPersistedServerClientRecordTimestamp() { michael@0: return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0); michael@0: } michael@0: michael@0: public void persistServerClientsTimestamp(long timestamp) { michael@0: getEditor().putLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, timestamp).commit(); michael@0: } michael@0: michael@0: public long getPersistedServerClientsTimestamp() { michael@0: return getPrefs().getLong(SyncConfiguration.CLIENTS_COLLECTION_TIMESTAMP, 0); michael@0: } michael@0: michael@0: public void purgeCryptoKeys() { michael@0: if (collectionKeys != null) { michael@0: collectionKeys.clear(); michael@0: } michael@0: persistedCryptoKeys().purge(); michael@0: } michael@0: michael@0: public void purgeMetaGlobal() { michael@0: metaGlobal = null; michael@0: persistedMetaGlobal().purge(); michael@0: } michael@0: michael@0: public PersistedCrypto5Keys persistedCryptoKeys() { michael@0: return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle); michael@0: } michael@0: michael@0: public PersistedMetaGlobal persistedMetaGlobal() { michael@0: return new PersistedMetaGlobal(getPrefs()); michael@0: } michael@0: }