/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.util.GeckoEventListener; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.util.Log; import java.util.Map; import java.util.HashMap; /** * Helper class to get, set, and observe Android Shared Preferences. */ public final class SharedPreferencesHelper implements GeckoEventListener { public static final String LOGTAG = "GeckoAndSharedPrefs"; // Calculate this once, at initialization. isLoggable is too expensive to // have in-line in each log call. private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); private enum Scope { APP("app"), PROFILE("profile"), GLOBAL("global"); public final String key; private Scope(String key) { this.key = key; } public static Scope forKey(String key) { for (Scope scope : values()) { if (scope.key.equals(key)) { return scope; } } throw new IllegalStateException("SharedPreferences scope must be valid."); } } protected final Context mContext; // mListeners is not synchronized because it is only updated in // handleObserve, which is called from Gecko serially. protected final Map mListeners; public SharedPreferencesHelper(Context context) { mContext = context; mListeners = new HashMap(); EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); if (dispatcher == null) { Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); return; } dispatcher.registerGeckoThreadListener(this, "SharedPreferences:Set", "SharedPreferences:Get", "SharedPreferences:Observe"); } public synchronized void uninit() { EventDispatcher dispatcher = GeckoApp.getEventDispatcher(); if (dispatcher == null) { Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException()); return; } dispatcher.unregisterGeckoThreadListener(this, "SharedPreferences:Set", "SharedPreferences:Get", "SharedPreferences:Observe"); } private SharedPreferences getSharedPreferences(JSONObject message) throws JSONException { final Scope scope = Scope.forKey(message.getString("scope")); switch (scope) { case APP: return GeckoSharedPrefs.forApp(mContext); case PROFILE: final String profileName = message.optString("profileName", null); if (profileName == null) { return GeckoSharedPrefs.forProfile(mContext); } else { return GeckoSharedPrefs.forProfileName(mContext, profileName); } case GLOBAL: final String branch = message.optString("branch", null); if (branch == null) { return PreferenceManager.getDefaultSharedPreferences(mContext); } else { return mContext.getSharedPreferences(branch, Context.MODE_PRIVATE); } } return null; } private String getBranch(Scope scope, String profileName, String branch) { switch (scope) { case APP: return GeckoSharedPrefs.APP_PREFS_NAME; case PROFILE: if (profileName == null) { profileName = GeckoProfile.get(mContext).getName(); } return GeckoSharedPrefs.PROFILE_PREFS_NAME_PREFIX + profileName; case GLOBAL: return branch; } return null; } /** * Set many SharedPreferences in Android. * * message.branch must exist, and should be a String SharedPreferences * branch name, or null for the default branch. * message.preferences should be an array of preferences. Each preference * must include a String name, a String type in ["bool", "int", "string"], * and an Object value. */ private void handleSet(JSONObject message) throws JSONException { SharedPreferences.Editor editor = getSharedPreferences(message).edit(); JSONArray jsonPrefs = message.getJSONArray("preferences"); for (int i = 0; i < jsonPrefs.length(); i++) { JSONObject pref = jsonPrefs.getJSONObject(i); String name = pref.getString("name"); String type = pref.getString("type"); if ("bool".equals(type)) { editor.putBoolean(name, pref.getBoolean("value")); } else if ("int".equals(type)) { editor.putInt(name, pref.getInt("value")); } else if ("string".equals(type)) { editor.putString(name, pref.getString("value")); } else { Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]"); } editor.apply(); } } /** * Get many SharedPreferences from Android. * * message.branch must exist, and should be a String SharedPreferences * branch name, or null for the default branch. * message.preferences should be an array of preferences. Each preference * must include a String name, and a String type in ["bool", "int", * "string"]. */ private JSONArray handleGet(JSONObject message) throws JSONException { SharedPreferences prefs = getSharedPreferences(message); JSONArray jsonPrefs = message.getJSONArray("preferences"); JSONArray jsonValues = new JSONArray(); for (int i = 0; i < jsonPrefs.length(); i++) { JSONObject pref = jsonPrefs.getJSONObject(i); String name = pref.getString("name"); String type = pref.getString("type"); JSONObject jsonValue = new JSONObject(); jsonValue.put("name", name); jsonValue.put("type", type); try { if ("bool".equals(type)) { boolean value = prefs.getBoolean(name, false); jsonValue.put("value", value); } else if ("int".equals(type)) { int value = prefs.getInt(name, 0); jsonValue.put("value", value); } else if ("string".equals(type)) { String value = prefs.getString(name, ""); jsonValue.put("value", value); } else { Log.w(LOGTAG, "Unknown pref value type [" + type + "] for pref [" + name + "]"); } } catch (ClassCastException e) { // Thrown if there is a preference with the given name that is // not the right type. Log.w(LOGTAG, "Wrong pref value type [" + type + "] for pref [" + name + "]"); } jsonValues.put(jsonValue); } return jsonValues; } private static class ChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { public final Scope scope; public final String branch; public final String profileName; public ChangeListener(final Scope scope, final String branch, final String profileName) { this.scope = scope; this.branch = branch; this.profileName = profileName; } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (logVerbose) { Log.v(LOGTAG, "Got onSharedPreferenceChanged"); } try { final JSONObject msg = new JSONObject(); msg.put("scope", this.scope.key); msg.put("branch", this.branch); msg.put("profileName", this.profileName); msg.put("key", key); // Truly, this is awful, but the API impedance is strong: there // is no way to get a single untyped value from a // SharedPreferences instance. msg.put("value", sharedPreferences.getAll().get(key)); GeckoAppShell.notifyObservers("SharedPreferences:Changed", msg.toString()); } catch (JSONException e) { Log.e(LOGTAG, "Got exception creating JSON object", e); return; } } } /** * Register or unregister a SharedPreferences.OnSharedPreferenceChangeListener. * * message.branch must exist, and should be a String SharedPreferences * branch name, or null for the default branch. * message.enable should be a boolean: true to enable listening, false to * disable listening. */ private void handleObserve(JSONObject message) throws JSONException { final SharedPreferences prefs = getSharedPreferences(message); final boolean enable = message.getBoolean("enable"); final Scope scope = Scope.forKey(message.getString("scope")); final String profileName = message.optString("profileName", null); final String branch = getBranch(scope, profileName, message.optString("branch", null)); if (branch == null) { Log.e(LOGTAG, "No branch specified for SharedPreference:Observe; aborting."); return; } // mListeners is only modified in this one observer, which is called // from Gecko serially. if (enable && !this.mListeners.containsKey(branch)) { SharedPreferences.OnSharedPreferenceChangeListener listener = new ChangeListener(scope, branch, profileName); this.mListeners.put(branch, listener); prefs.registerOnSharedPreferenceChangeListener(listener); } if (!enable && this.mListeners.containsKey(branch)) { SharedPreferences.OnSharedPreferenceChangeListener listener = this.mListeners.remove(branch); prefs.unregisterOnSharedPreferenceChangeListener(listener); } } @Override public void handleMessage(String event, JSONObject message) { // Everything here is synchronous and serial, so we need not worry about // overwriting an in-progress response. try { if (event.equals("SharedPreferences:Set")) { if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Set message."); } handleSet(message); } else if (event.equals("SharedPreferences:Get")) { if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Get message."); } JSONObject obj = new JSONObject(); obj.put("values", handleGet(message)); EventDispatcher.sendResponse(message, obj); } else if (event.equals("SharedPreferences:Observe")) { if (logVerbose) { Log.v(LOGTAG, "Got SharedPreferences:Observe message."); } handleObserve(message); } else { Log.e(LOGTAG, "SharedPreferencesHelper got unexpected message " + event); return; } } catch (JSONException e) { Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e); return; } } }