summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
blob: a2d80788c2b4a9072b44a88ed58d2c411a5cbd4b (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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
/* -*- 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.home;

import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Locale;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend;
import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomeConfig.State;
import org.mozilla.gecko.util.HardwareUtils;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.support.annotation.CheckResult;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;

public class HomeConfigPrefsBackend implements HomeConfigBackend {
    private static final String LOGTAG = "GeckoHomeConfigBackend";

    // Increment this to trigger a migration.
    @VisibleForTesting
    static final int VERSION = 8;

    // This key was originally used to store only an array of panel configs.
    public static final String PREFS_CONFIG_KEY_OLD = "home_panels";

    // This key is now used to store a version number with the array of panel configs.
    public static final String PREFS_CONFIG_KEY = "home_panels_with_version";

    // Keys used with JSON object stored in prefs.
    private static final String JSON_KEY_PANELS = "panels";
    private static final String JSON_KEY_VERSION = "version";

    private static final String PREFS_LOCALE_KEY = "home_locale";

    private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload";

    private final Context mContext;
    private ReloadBroadcastReceiver mReloadBroadcastReceiver;
    private OnReloadListener mReloadListener;

    private static boolean sMigrationDone;

    public HomeConfigPrefsBackend(Context context) {
        mContext = context;
    }

    private SharedPreferences getSharedPreferences() {
        return GeckoSharedPrefs.forProfile(mContext);
    }

    private State loadDefaultConfig() {
        final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();

        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
                                                  EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));

        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));


        return new State(panelConfigs, true);
    }

    /**
     * Iterate through the panels to check if they are all disabled.
     */
    private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
        final int count = jsonPanels.length();
        for (int i = 0; i < count; i++) {
            final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);

            if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
                return false;
            }
        }

        return true;
    }

    protected enum Position {
        NONE, // Not present.
        FRONT, // At the front of the list of panels.
        BACK, // At the back of the list of panels.
    }

    /**
     * Create and insert a built-in panel configuration.
     *
     * @param context Android context.
     * @param jsonPanels array of JSON panels to update in place.
     * @param panelType to add.
     * @param positionOnPhones where to place the new panel on phones.
     * @param positionOnTablets where to place the new panel on tablets.
     * @throws JSONException
     */
    protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels,
            PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException {
        // Add the new panel.
        final JSONObject jsonPanelConfig =
                createBuiltinPanelConfig(context, panelType).toJSON();

        // If any panel is enabled, then we should make the new panel enabled.
        jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED,
                                 allPanelsAreDisabled(jsonPanels));

        final boolean isTablet = HardwareUtils.isTablet();
        final boolean isPhone = !isTablet;

        // Maybe add the new panel to the front of the array.
        if ((isPhone && positionOnPhones == Position.FRONT) ||
            (isTablet && positionOnTablets == Position.FRONT)) {
            // This is an inefficient way to stretch [a, b, c] to [a, a, b, c].
            for (int i = jsonPanels.length(); i >= 1; i--) {
                jsonPanels.put(i, jsonPanels.get(i - 1));
            }
            // And this inserts [d, a, b, c].
            jsonPanels.put(0, jsonPanelConfig);
        }

        // Maybe add the new panel to the back of the array.
        if ((isPhone && positionOnPhones == Position.BACK) ||
            (isTablet && positionOnTablets == Position.BACK)) {
            jsonPanels.put(jsonPanelConfig);
        }
    }

    /**
     * Updates the panels to combine the History and Sync panels into the (Combined) History panel.
     *
     * Tries to replace the History panel with the Combined History panel if visible, or falls back to
     * replacing the Sync panel if it's visible. That way, we minimize panel reordering during a migration.
     * @param context Android context
     * @param jsonPanels array of original JSON panels
     * @return new array of updated JSON panels
     * @throws JSONException
     */
    private static JSONArray combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels) throws JSONException {
        EnumSet<PanelConfig.Flags> historyFlags = null;
        EnumSet<PanelConfig.Flags> syncFlags = null;

        int historyIndex = -1;
        int syncIndex = -1;

        // Determine state and location of History and Sync panels.
        for (int i = 0; i < jsonPanels.length(); i++) {
            JSONObject panelObj = jsonPanels.getJSONObject(i);
            final PanelConfig panelConfig = new PanelConfig(panelObj);
            final PanelType type = panelConfig.getType();
            if (type == PanelType.DEPRECATED_HISTORY) {
                historyIndex = i;
                historyFlags = panelConfig.getFlags();
            } else if (type == PanelType.DEPRECATED_REMOTE_TABS) {
                syncIndex = i;
                syncFlags = panelConfig.getFlags();
            } else if (type == PanelType.COMBINED_HISTORY) {
                // Partial landing of bug 1220928 combined the History and Sync panels of users who didn't
                // have home panel customizations (including new users), thus they don't this migration.
                return jsonPanels;
            }
        }

        if (historyIndex == -1 || syncIndex == -1) {
            throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
        }

        PanelConfig newPanel;
        int replaceIndex;
        int removeIndex;

        if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
            // Replace the Sync panel if it's visible and the History panel is disabled.
            replaceIndex = syncIndex;
            removeIndex = historyIndex;
            newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags);
        } else {
            // Otherwise, just replace the History panel.
            replaceIndex = historyIndex;
            removeIndex = syncIndex;
            newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, historyFlags);
        }

        // Copy the array with updated panel and removed panel.
        final JSONArray newArray = new JSONArray();
        for (int i = 0; i < jsonPanels.length(); i++) {
            if (i == replaceIndex) {
                newArray.put(newPanel.toJSON());
            } else if (i == removeIndex) {
                continue;
            } else {
                newArray.put(jsonPanels.get(i));
            }
        }

        return newArray;
    }

    /**
     * Iterate over all homepanels to verify that there is at least one default panel. If there is
     * no default panel, set History as the default panel. (This is only relevant for two botched
     * migrations where the history panel should have been made the default panel, but wasn't.)
     */
    private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
        int historyIndex = -1;

        // If all panels are disabled, there is no default panel - this is the only valid state
        // that has no default. We can use this flag to track whether any visible panels have been
        // found.
        boolean enabledPanelsFound = false;

        for (int i = 0; i < jsonPanels.length(); i++) {
            final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
            if (panelConfig.isDefault()) {
                return;
            }

            if (!panelConfig.isDisabled()) {
                enabledPanelsFound = true;
            }

            if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
                historyIndex = i;
            }
        }

        if (!enabledPanelsFound) {
            // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
            // for more information).
            return;
        }

        // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
        final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
        jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
    }

    /**
     * Removes a panel from the home panel config.
     * If the removed panel was set as the default home panel, we provide a replacement for it.
     *
     * @param context Android context
     * @param jsonPanels array of original JSON panels
     * @param panelToRemove The home panel to be removed.
     * @param replacementPanel The panel which will replace it if the removed panel
     *                         was the default home panel.
     * @param alwaysUnhide If true, the replacement panel will always be unhidden,
     *                     otherwise only if we turn it into the new default panel.
     * @return new array of updated JSON panels
     * @throws JSONException
     */
    private static JSONArray removePanel(Context context, JSONArray jsonPanels,
                                         PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
        boolean wasDefault = false;
        boolean wasDisabled = false;
        int replacementPanelIndex = -1;
        boolean replacementWasDefault = false;

        // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
        // the items we don't want deleted into a new array.
        final JSONArray newJSONPanels = new JSONArray();

        for (int i = 0; i < jsonPanels.length(); i++) {
            final JSONObject panelJSON = jsonPanels.getJSONObject(i);
            final PanelConfig panelConfig = new PanelConfig(panelJSON);

            if (panelConfig.getType() == panelToRemove) {
                // If this panel was the default we'll need to assign a new default:
                wasDefault = panelConfig.isDefault();
                wasDisabled = panelConfig.isDisabled();
            } else {
                if (panelConfig.getType() == replacementPanel) {
                    replacementPanelIndex = newJSONPanels.length();
                    if (panelConfig.isDefault()) {
                        replacementWasDefault = true;
                    }
                }

                newJSONPanels.put(panelJSON);
            }
        }

        // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
        // to be the new default panel, since a hidden default panel doesn't make sense.
        // This is to allow preserving the behaviour of the original reading list migration function.
        if ((wasDefault || alwaysUnhide) && !wasDisabled) {
            final JSONObject replacementPanelConfig;
            if (wasDefault) {
                // If the removed panel was the default, the replacement has to be made the new default
                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
            } else {
                final EnumSet<HomeConfig.PanelConfig.Flags> flags;
                if (replacementWasDefault) {
                    // However if the replacement panel was already default, we need to preserve it's default status
                    // (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired
                    // flags are retained - in this case there's only DEFAULT_PANEL, which is mutually
                    // exclusive with the DISABLE_PANEL case).
                    flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL);
                } else {
                    flags = EnumSet.noneOf(PanelConfig.Flags.class);
                }

                // The panel is visible since we don't set Flags.DISABLED_PANEL.
                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON();
            }

            if (replacementPanelIndex != -1) {
                newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
            } else {
                newJSONPanels.put(replacementPanelConfig);
            }
        }

        return newJSONPanels;
    }

    /**
     * Checks to see if the reading list panel already exists.
     *
     * @param jsonPanels JSONArray array representing the curent set of panel configs.
     *
     * @return boolean Whether or not the reading list panel exists.
     */
    private static boolean readingListPanelExists(JSONArray jsonPanels) {
        final int count = jsonPanels.length();
        for (int i = 0; i < count; i++) {
            try {
                final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
                final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
                if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
                    return true;
                }
            } catch (Exception e) {
                // It's okay to ignore this exception, since an invalid reading list
                // panel config is equivalent to no reading list panel.
                Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
            }
        }
        return false;
    }

    @CheckResult
    static synchronized JSONArray migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion,
                                                              final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor) throws JSONException {

        JSONArray jsonPanels = jsonPanelsIn;

        for (int v = currentVersion + 1; v <= newVersion; v++) {
            Log.d(LOGTAG, "Migrating to version = " + v);

            switch (v) {
                case 1:
                    // Add "Recent Tabs" panel.
                    addBuiltinPanelConfig(context, jsonPanels,
                            PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);

                    // Remove the old pref key.
                    prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
                    break;

                case 2:
                    // Add "Remote Tabs"/"Synced Tabs" panel.
                    addBuiltinPanelConfig(context, jsonPanels,
                            PanelType.DEPRECATED_REMOTE_TABS, Position.FRONT, Position.BACK);
                    break;

                case 3:
                    // Add the "Reading List" panel if it does not exist. At one time,
                    // the Reading List panel was shown only to devices that were not
                    // considered "low memory". Now, we expose the panel to all devices.
                    // This migration should only occur for "low memory" devices.
                    // Note: This will not agree with the default configuration, which
                    // has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices.
                    if (!readingListPanelExists(jsonPanels)) {
                        addBuiltinPanelConfig(context, jsonPanels,
                                PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK);
                    }
                    break;

                case 4:
                    // Combine the History and Sync panels. In order to minimize an unexpected reordering
                    // of panels, we try to replace the History panel if it's visible, and fall back to
                    // the Sync panel if that's visible.
                    jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
                    break;

                case 5:
                    // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
                    ensureDefaultPanelForV5orV8(context, jsonPanels);
                    break;

                case 6:
                    jsonPanels = removePanel(context, jsonPanels,
                            PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
                    break;

                case 7:
                    jsonPanels = removePanel(context, jsonPanels,
                            PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
                    break;

                case 8:
                    // Similar to "case 5" above, this time 1304777 - once again we lost track
                    // of the history panel
                    ensureDefaultPanelForV5orV8(context, jsonPanels);
                    break;
            }
        }

        return jsonPanels;
    }

    /**
     * Migrates JSON config data storage.
     *
     * @param context Context used to get shared preferences and create built-in panel.
     * @param jsonString String currently stored in preferences.
     *
     * @return JSONArray array representing new set of panel configs.
     */
    private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
        // If the migration is already done, we're at the current version.
        if (sMigrationDone) {
            final JSONObject json = new JSONObject(jsonString);
            return json.getJSONArray(JSON_KEY_PANELS);
        }

        // Make sure we only do this version check once.
        sMigrationDone = true;

        JSONArray jsonPanels;
        final int version;

        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
        if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
            // Our original implementation did not contain versioning, so this is implicitly version 0.
            jsonPanels = new JSONArray(jsonString);
            version = 0;
        } else {
            final JSONObject json = new JSONObject(jsonString);
            jsonPanels = json.getJSONArray(JSON_KEY_PANELS);
            version = json.getInt(JSON_KEY_VERSION);
        }

        if (version == VERSION) {
            return jsonPanels;
        }

        Log.d(LOGTAG, "Performing migration");

        final SharedPreferences.Editor prefsEditor = prefs.edit();

        jsonPanels = migratePrefsFromVersionToVersion(context, version, VERSION, jsonPanels, prefsEditor);

        // Save the new panel config and the new version number.
        final JSONObject newJson = new JSONObject();
        newJson.put(JSON_KEY_PANELS, jsonPanels);
        newJson.put(JSON_KEY_VERSION, VERSION);

        prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
        prefsEditor.apply();

        return jsonPanels;
    }

    private State loadConfigFromString(String jsonString) {
        final JSONArray jsonPanelConfigs;
        try {
            jsonPanelConfigs = maybePerformMigration(mContext, jsonString);
            updatePrefsFromConfig(jsonPanelConfigs);
        } catch (JSONException e) {
            Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e);

            // Fallback to default config
            return loadDefaultConfig();
        }

        final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();

        final int count = jsonPanelConfigs.length();
        for (int i = 0; i < count; i++) {
            try {
                final JSONObject jsonPanelConfig = jsonPanelConfigs.getJSONObject(i);
                final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
                panelConfigs.add(panelConfig);
            } catch (Exception e) {
                Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
            }
        }

        return new State(panelConfigs, false);
    }

    @Override
    public State load() {
        final SharedPreferences prefs = getSharedPreferences();

        final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY);
        final String jsonString = prefs.getString(key, null);

        final State configState;
        if (TextUtils.isEmpty(jsonString)) {
            configState = loadDefaultConfig();
        } else {
            configState = loadConfigFromString(jsonString);
        }

        return configState;
    }

    @Override
    public void save(State configState) {
        final SharedPreferences prefs = getSharedPreferences();
        final SharedPreferences.Editor editor = prefs.edit();

        // No need to save the state to disk if it represents the default
        // HomeConfig configuration. Simply force all existing HomeConfigLoader
        // instances to refresh their contents.
        if (!configState.isDefault()) {
            final JSONArray jsonPanelConfigs = new JSONArray();

            for (PanelConfig panelConfig : configState) {
                try {
                    final JSONObject jsonPanelConfig = panelConfig.toJSON();
                    jsonPanelConfigs.put(jsonPanelConfig);
                } catch (Exception e) {
                    Log.e(LOGTAG, "Exception converting PanelConfig to JSON", e);
                }
            }

            try {
                final JSONObject json = new JSONObject();
                json.put(JSON_KEY_PANELS, jsonPanelConfigs);
                json.put(JSON_KEY_VERSION, VERSION);

                editor.putString(PREFS_CONFIG_KEY, json.toString());
            } catch (JSONException e) {
                Log.e(LOGTAG, "Exception saving PanelConfig state", e);
            }
        }

        editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString());
        editor.apply();

        // Trigger reload listeners on all live backend instances
        sendReloadBroadcast();
    }

    @Override
    public String getLocale() {
        final SharedPreferences prefs = getSharedPreferences();

        String locale = prefs.getString(PREFS_LOCALE_KEY, null);
        if (locale == null) {
            // Initialize config with the current locale
            final String currentLocale = Locale.getDefault().toString();

            final SharedPreferences.Editor editor = prefs.edit();
            editor.putString(PREFS_LOCALE_KEY, currentLocale);
            editor.apply();

            // If the user has saved HomeConfig before, return null this
            // one time to trigger a refresh and ensure we use the
            // correct locale for the saved state. For more context,
            // see HomePanelsManager.onLocaleReady().
            if (!prefs.contains(PREFS_CONFIG_KEY)) {
                locale = currentLocale;
            }
        }

        return locale;
    }

    @Override
    public void setOnReloadListener(OnReloadListener listener) {
        if (mReloadListener != null) {
            unregisterReloadReceiver();
            mReloadBroadcastReceiver = null;
        }

        mReloadListener = listener;

        if (mReloadListener != null) {
            mReloadBroadcastReceiver = new ReloadBroadcastReceiver();
            registerReloadReceiver();
        }
    }

    /**
     * Update prefs that depend on home panels state.
     *
     * This includes the prefs that keep track of whether bookmarks or history are enabled, which are
     * used to control the visibility of the corresponding menu items.
     */
    private void updatePrefsFromConfig(JSONArray panelsArray) {
        final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mContext);
        if (!prefs.contains(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED)
                || !prefs.contains(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED)) {

            final String bookmarkType = PanelType.BOOKMARKS.toString();
            final String historyType = PanelType.COMBINED_HISTORY.toString();
            try {
                for (int i = 0; i < panelsArray.length(); i++) {
                    final JSONObject panelObj = panelsArray.getJSONObject(i);
                    final String panelType = panelObj.optString(PanelConfig.JSON_KEY_TYPE, null);
                    if (panelType == null) {
                        break;
                    }
                    final boolean isDisabled = panelObj.optBoolean(PanelConfig.JSON_KEY_DISABLED, false);
                    if (bookmarkType.equals(panelType)) {
                        prefs.edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, !isDisabled).apply();
                    } else if (historyType.equals(panelType)) {
                        prefs.edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, !isDisabled).apply();
                    }
                }
            } catch (JSONException e) {
                Log.e(LOGTAG, "Error fetching panel from config to update prefs");
            }
        }
    }


    private void sendReloadBroadcast() {
        final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
        final Intent reloadIntent = new Intent(RELOAD_BROADCAST);
        lbm.sendBroadcast(reloadIntent);
    }

    private void registerReloadReceiver() {
        final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
        lbm.registerReceiver(mReloadBroadcastReceiver, new IntentFilter(RELOAD_BROADCAST));
    }

    private void unregisterReloadReceiver() {
        final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
        lbm.unregisterReceiver(mReloadBroadcastReceiver);
    }

    private class ReloadBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            mReloadListener.onReload();
        }
    }
}