/** * Funf: Open Sensing Framework * Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland. * Acknowledgments: Alan Gardner * Contact: nadav@media.mit.edu * * This file is part of Funf. * * Funf is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Funf is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Funf. If not, see <http://www.gnu.org/licenses/>. */ package edu.mit.media.funf.configured; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; import edu.mit.media.funf.EqualsUtil; import edu.mit.media.funf.Utils; import edu.mit.media.funf.probe.ProbeExceptions.UnstorableTypeException; /** * A convenience interface to access a Funf configuration stored in a SharedPreferences. * All data is stored in the SharedPreferences, and all edits follow the same transaction * model as SharedPreferences. * */ public class FunfConfig implements OnSharedPreferenceChangeListener { public static final String NAME_KEY = "name", VERSION_KEY = "version", CONFIG_UPDATE_URL_KEY = "configUpdateUrl", CONFIG_UPDATE_PERIOD_KEY = "configUpdatePeriod", DATA_UPLOAD_URL_KEY = "dataUploadUrl", DATA_UPLOAD_PERIOD_KEY = "dataUploadPeriod", DATA_ARCHIVE_PERIOD_KEY = "dataArchivePeriod", DATA_REQUESTS_KEY = "dataRequests"; public static final long DEFAULT_VERSION = 0, DEFAULT_DATA_ARCHIVE_PERIOD = 3 * 60 * 60, // 3 hours DEFAULT_DATA_UPLOAD_PERIOD = 6 * 60 * 60, // 6 hours DEFAULT_CONFIG_UPDATE_PERIOD = 1 * 60 * 60; // 1 hour public static final String DEFAULT_DATA_REQUESTS = "{}"; // No requests private final SharedPreferences prefs; private FunfConfig(SharedPreferences prefs) { assert prefs != null; this.prefs = prefs; prefs.registerOnSharedPreferenceChangeListener(this); dataRequests = new HashMap<String, Bundle[]>(); } private static final Map<SharedPreferences, FunfConfig> instances = new HashMap<SharedPreferences, FunfConfig>(); public static FunfConfig getInstance(SharedPreferences prefs) { FunfConfig config = instances.get(prefs); if (config == null) { synchronized (instances) { // Check one more time when we are synchronized config = instances.get(prefs); if (config == null) { config = new FunfConfig(prefs); instances.put(prefs, config); } } } return config; } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (sharedPreferences == prefs && isDataRequestKey(key)) { synchronized (dataRequests) { dataRequests.remove(keyToProbename(key)); } } } public String getName() { return prefs.getString(NAME_KEY, null); } public long getVersion() { return prefs.getLong(VERSION_KEY, DEFAULT_VERSION); } public String getConfigUpdateUrl() { return prefs.getString(CONFIG_UPDATE_URL_KEY, null); } public long getConfigUpdatePeriod() { return prefs.getLong(CONFIG_UPDATE_PERIOD_KEY, DEFAULT_CONFIG_UPDATE_PERIOD); } public String getDataUploadUrl() { return prefs.getString(DATA_UPLOAD_URL_KEY, null); } public long getDataUploadPeriod() { return prefs.getLong(DATA_UPLOAD_PERIOD_KEY, DEFAULT_DATA_UPLOAD_PERIOD); } public long getDataArchivePeriod() { return prefs.getLong(DATA_ARCHIVE_PERIOD_KEY, DEFAULT_DATA_ARCHIVE_PERIOD); } private Map<String, Bundle[]> dataRequests; // cache /** * Returns a copy of the data requests that can be modified by the users, * without affecting the configuration object. * @return */ public Map<String, Bundle[]> getDataRequests() { Set<String> probeNames = prefs.getAll().keySet(); synchronized (dataRequests) { // Make sure all keys have been cached for (String key : probeNames) { if (isDataRequestKey(key)) { String probeName = keyToProbename(key); if (!dataRequests.containsKey(probeName)) { getDataRequests(probeName); } } } return deepCopy(dataRequests); // Deep copy so users can modify } } public Bundle[] getDataRequests(String probeName) { synchronized (dataRequests) { // Check to see if it is cached first if (dataRequests.containsKey(probeName)) { return dataRequests.get(probeName); } // Check to see if we have a key for this String jsonString = prefs.getString(probeNameToKey(probeName), null); if (jsonString == null) { return null; } // If so parse value and store in cache try { JSONArray jsonArray = new JSONArray(jsonString); Bundle[] requests = getBundleArray(jsonArray); dataRequests.put(probeName, requests); return requests; } catch (JSONException e) { throw new RuntimeException(e); } } } private Map<String, Bundle[]> deepCopy(Map<String, Bundle[]> original) { Map<String,Bundle[]> copy = new HashMap<String, Bundle[]>(); for (Map.Entry<String, Bundle[]> entry : original.entrySet()) { Bundle[] originalBundleArray = entry.getValue(); Bundle[] copyBundleArray = new Bundle[originalBundleArray.length]; for (int i=0; i<originalBundleArray.length; i++) { copyBundleArray[i] = new Bundle(originalBundleArray[i]); } copy.put(entry.getKey(), copyBundleArray); } return copy; } public SharedPreferences getPrefs() { return prefs; } public Editor edit() { return new Editor(); } public class Editor { private SharedPreferences.Editor editor = getPrefs().edit(); private Set<String> changedProbes = new HashSet<String>(); private boolean clear = false; public Editor setName(String name) { editor.putString(NAME_KEY, name); return this; } public Editor setVersion(int version) { editor.putInt(VERSION_KEY, version); return this; } public Editor setConfigUpdateUrl(String configUpdateUrl) { editor.putString(CONFIG_UPDATE_URL_KEY, configUpdateUrl); return this; } public Editor setConfigUpdatePeriod(long configUpdatePeriod) { editor.putLong(CONFIG_UPDATE_PERIOD_KEY, configUpdatePeriod); return this; } public Editor setDataUploadUrl(String dataUploadUrl) { editor.putString(DATA_UPLOAD_URL_KEY, dataUploadUrl); return this; } public Editor setDataUploadPeriod(long dataUploadPeriod) { editor.putLong(DATA_UPLOAD_PERIOD_KEY, dataUploadPeriod); return this; } public Editor setDataArchivePeriod(long dataArchivePeriod) { editor.putLong(DATA_ARCHIVE_PERIOD_KEY, dataArchivePeriod); return this; } public Editor setDataRequests(Map<String, Bundle[]> dataRequests) { // Remove all of the items that don't exist in the new data requests for (String existingProbeName : getDataRequests().keySet()) { if (!dataRequests.containsKey(existingProbeName)) { editor.remove(probeNameToKey(existingProbeName)); changedProbes.add(existingProbeName); } } for (Map.Entry<String, Bundle[]> dataRequestEntry : dataRequests.entrySet()) { setDataRequest(dataRequestEntry.getKey(), dataRequestEntry.getValue()); } return this; } public Editor setDataRequest(String probeName, Bundle[] requests) { if (!EqualsUtil.areEqual(getDataRequests(probeName), requests)) { if (requests == null || requests.length == 0) { editor.remove(probeNameToKey(probeName)); } else { editor.putString(probeNameToKey(probeName), toJSONArray(requests).toString()); } changedProbes.add(probeName); } return this; } private void setString(JSONObject jsonObject, String key) { String value = jsonObject.optString(key, null); if (value == null) { editor.remove(key); } else { editor.putString(key, value); } } private void setPositiveLong(JSONObject jsonObject, String key) { long value = jsonObject.optLong(key, 0L); if (value <= 0) { editor.remove(key); } else { editor.putLong(key, value); } } public Editor setAll(String jsonString) throws JSONException { JSONObject jsonObject = new JSONObject(jsonString); editor.clear(); clear = true; setString(jsonObject, NAME_KEY); setPositiveLong(jsonObject, VERSION_KEY); setString(jsonObject, CONFIG_UPDATE_URL_KEY); setPositiveLong(jsonObject, CONFIG_UPDATE_PERIOD_KEY); setString(jsonObject, DATA_UPLOAD_URL_KEY); setPositiveLong(jsonObject, DATA_UPLOAD_PERIOD_KEY); setPositiveLong(jsonObject, DATA_ARCHIVE_PERIOD_KEY); // Add new probe requests JSONObject requestsJsonObject = jsonObject.getJSONObject(DATA_REQUESTS_KEY); Iterator requestsIterator = requestsJsonObject.keys(); while (requestsIterator.hasNext()) { String probeName = (String)requestsIterator.next(); JSONArray value = requestsJsonObject.getJSONArray(probeName); editor.putString(probeNameToKey(probeName), value.toString()); } return this; } public Editor setAll(FunfConfig otherConfig) { editor.clear(); clear = true; for (Map.Entry<String, ?> entry : otherConfig.getPrefs().getAll().entrySet()) { if (entry.getValue() != null) { Utils.putInPrefs(editor, entry.getKey(), entry.getValue()); } } return this; } public Editor clear() { editor.clear(); clear = true; return this; } public boolean commit() { if (clear || !changedProbes.isEmpty()) { synchronized (dataRequests) { if (clear) { dataRequests.clear(); } else { for (String changedProbeName : changedProbes) { dataRequests.remove(changedProbeName); } } return editor.commit(); // Commit in synchronized block to prevent stale data request caches } } else { return editor.commit(); // Don't need to synchronize } } } public static boolean isDataRequestKey(String key) { return key != null && key.startsWith(DATA_REQUESTS_KEY); } public static String probeNameToKey(String probeName) { return DATA_REQUESTS_KEY + probeName; } public static String keyToProbename(String key) { return key.substring(DATA_REQUESTS_KEY.length()); } private static JSONObject getDataRequestJsonObject(Map<String, Bundle[]> dataRequestMap) { JSONObject dataRequestsJson = new JSONObject(); try { for (Map.Entry<String, Bundle[]> dataReqest : dataRequestMap.entrySet()) { dataRequestsJson.put(dataReqest.getKey(), toJSONArray(dataReqest.getValue())); } } catch (JSONException e) { throw new RuntimeException(e); } return dataRequestsJson; } private static Map<String, Bundle[]> getDataRequestMap(JSONObject dataRequestsObject) { Map<String, Bundle[]> dataRequestMap = new HashMap<String, Bundle[]>(); Iterator<String> probeNames = dataRequestsObject.keys(); try { while (probeNames.hasNext()) { String probeName = probeNames.next(); JSONArray requestsJsonArray = dataRequestsObject.getJSONArray(probeName); dataRequestMap.put(probeName, getBundleArray(requestsJsonArray)); } } catch (JSONException e) { throw new RuntimeException(e); } return dataRequestMap; } private static Bundle[] getBundleArray(JSONArray jsonArray) throws JSONException { Bundle[] request = new Bundle[jsonArray.length()]; for (int i = 0; i < jsonArray.length(); i++) { request[i] = getBundle(jsonArray.getJSONObject(i)); } return request; } private static JSONArray toJSONArray(Bundle[] bundles) { JSONArray jsonArray = new JSONArray(); for (Bundle bundle : bundles) { jsonArray.put(toJSONObject(bundle)); } return jsonArray; } @SuppressWarnings("unchecked") private static Bundle getBundle(JSONObject jsonObject) throws JSONException { Bundle requestPart = new Bundle(); Iterator<String> paramNames = jsonObject.keys(); while (paramNames.hasNext()) { String paramName = paramNames.next(); try { Utils.putInBundle(requestPart, paramName, jsonObject.get(paramName)); } catch (UnstorableTypeException e) { throw new JSONException(e.getLocalizedMessage()); } } return requestPart; } @Override public boolean equals(Object o) { return o != null && o instanceof FunfConfig && (prefs == ((FunfConfig)o).prefs // prefs is singleton || prefs.getAll().equals(((FunfConfig)o).prefs.getAll())); // All internal values are the same } @Override public int hashCode() { return prefs.hashCode(); } @Override public String toString() { try { return toJsonObject().toString(); } catch (JSONException e) { // Swallowed to prevent crashes in debugger return super.toString(); } } public String toString(boolean prettyPrint) { try { return prettyPrint ? toJsonObject().toString(4) : toString(); } catch (JSONException e) { throw new RuntimeException(e); } } JSONObject toJsonObject() throws JSONException { JSONObject jsonObject = new JSONObject(); JSONObject dataRequests = new JSONObject(); jsonObject.put(DATA_REQUESTS_KEY, dataRequests); for (Map.Entry<String, ?> entry : prefs.getAll().entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (isDataRequestKey(key)) { if (value != null) { String probeName = keyToProbename(key); dataRequests.put(probeName, new JSONArray((String)value)); } } else { jsonObject.put(key, value); } } return jsonObject; } private static JSONObject toJSONObject(Bundle bundle) { return new JSONObject(Utils.getValues(bundle)); } }