summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
blob: 1066da0799daaa694a49a0208096f51774e16101 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/* -*- 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<String, SharedPreferences.OnSharedPreferenceChangeListener> mListeners;

    public SharedPreferencesHelper(Context context) {
        mContext = context;

        mListeners = new HashMap<String, SharedPreferences.OnSharedPreferenceChangeListener>();

        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;
        }
    }
}