1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.gecko.home;
7 
8 import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
9 
10 import java.util.ArrayList;
11 import java.util.EnumSet;
12 import java.util.Locale;
13 
14 import org.json.JSONArray;
15 import org.json.JSONException;
16 import org.json.JSONObject;
17 import org.mozilla.gecko.GeckoSharedPrefs;
18 import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend;
19 import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
20 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
21 import org.mozilla.gecko.home.HomeConfig.PanelType;
22 import org.mozilla.gecko.home.HomeConfig.State;
23 import org.mozilla.gecko.util.HardwareUtils;
24 
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.SharedPreferences;
30 import android.support.annotation.CheckResult;
31 import android.support.annotation.VisibleForTesting;
32 import android.support.v4.content.LocalBroadcastManager;
33 import android.text.TextUtils;
34 import android.util.Log;
35 
36 public class HomeConfigPrefsBackend implements HomeConfigBackend {
37     private static final String LOGTAG = "GeckoHomeConfigBackend";
38 
39     // Increment this to trigger a migration.
40     @VisibleForTesting
41     static final int VERSION = 8;
42 
43     // This key was originally used to store only an array of panel configs.
44     public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
45 
46     // This key is now used to store a version number with the array of panel configs.
47     public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
48 
49     // Keys used with JSON object stored in prefs.
50     private static final String JSON_KEY_PANELS = "panels";
51     private static final String JSON_KEY_VERSION = "version";
52 
53     private static final String PREFS_LOCALE_KEY = "home_locale";
54 
55     private static final String RELOAD_BROADCAST = "HomeConfigPrefsBackend:Reload";
56 
57     private final Context mContext;
58     private ReloadBroadcastReceiver mReloadBroadcastReceiver;
59     private OnReloadListener mReloadListener;
60 
61     private static boolean sMigrationDone;
62 
HomeConfigPrefsBackend(Context context)63     public HomeConfigPrefsBackend(Context context) {
64         mContext = context;
65     }
66 
getSharedPreferences()67     private SharedPreferences getSharedPreferences() {
68         return GeckoSharedPrefs.forProfile(mContext);
69     }
70 
loadDefaultConfig()71     private State loadDefaultConfig() {
72         final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
73 
74         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
75                                                   EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
76 
77         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
78         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
79 
80 
81         return new State(panelConfigs, true);
82     }
83 
84     /**
85      * Iterate through the panels to check if they are all disabled.
86      */
allPanelsAreDisabled(JSONArray jsonPanels)87     private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
88         final int count = jsonPanels.length();
89         for (int i = 0; i < count; i++) {
90             final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
91 
92             if (!jsonPanelConfig.optBoolean(PanelConfig.JSON_KEY_DISABLED, false)) {
93                 return false;
94             }
95         }
96 
97         return true;
98     }
99 
100     protected enum Position {
101         NONE, // Not present.
102         FRONT, // At the front of the list of panels.
103         BACK, // At the back of the list of panels.
104     }
105 
106     /**
107      * Create and insert a built-in panel configuration.
108      *
109      * @param context Android context.
110      * @param jsonPanels array of JSON panels to update in place.
111      * @param panelType to add.
112      * @param positionOnPhones where to place the new panel on phones.
113      * @param positionOnTablets where to place the new panel on tablets.
114      * @throws JSONException
115      */
addBuiltinPanelConfig(Context context, JSONArray jsonPanels, PanelType panelType, Position positionOnPhones, Position positionOnTablets)116     protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels,
117             PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException {
118         // Add the new panel.
119         final JSONObject jsonPanelConfig =
120                 createBuiltinPanelConfig(context, panelType).toJSON();
121 
122         // If any panel is enabled, then we should make the new panel enabled.
123         jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED,
124                                  allPanelsAreDisabled(jsonPanels));
125 
126         final boolean isTablet = HardwareUtils.isTablet();
127         final boolean isPhone = !isTablet;
128 
129         // Maybe add the new panel to the front of the array.
130         if ((isPhone && positionOnPhones == Position.FRONT) ||
131             (isTablet && positionOnTablets == Position.FRONT)) {
132             // This is an inefficient way to stretch [a, b, c] to [a, a, b, c].
133             for (int i = jsonPanels.length(); i >= 1; i--) {
134                 jsonPanels.put(i, jsonPanels.get(i - 1));
135             }
136             // And this inserts [d, a, b, c].
137             jsonPanels.put(0, jsonPanelConfig);
138         }
139 
140         // Maybe add the new panel to the back of the array.
141         if ((isPhone && positionOnPhones == Position.BACK) ||
142             (isTablet && positionOnTablets == Position.BACK)) {
143             jsonPanels.put(jsonPanelConfig);
144         }
145     }
146 
147     /**
148      * Updates the panels to combine the History and Sync panels into the (Combined) History panel.
149      *
150      * Tries to replace the History panel with the Combined History panel if visible, or falls back to
151      * replacing the Sync panel if it's visible. That way, we minimize panel reordering during a migration.
152      * @param context Android context
153      * @param jsonPanels array of original JSON panels
154      * @return new array of updated JSON panels
155      * @throws JSONException
156      */
combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels)157     private static JSONArray combineHistoryAndSyncPanels(Context context, JSONArray jsonPanels) throws JSONException {
158         EnumSet<PanelConfig.Flags> historyFlags = null;
159         EnumSet<PanelConfig.Flags> syncFlags = null;
160 
161         int historyIndex = -1;
162         int syncIndex = -1;
163 
164         // Determine state and location of History and Sync panels.
165         for (int i = 0; i < jsonPanels.length(); i++) {
166             JSONObject panelObj = jsonPanels.getJSONObject(i);
167             final PanelConfig panelConfig = new PanelConfig(panelObj);
168             final PanelType type = panelConfig.getType();
169             if (type == PanelType.DEPRECATED_HISTORY) {
170                 historyIndex = i;
171                 historyFlags = panelConfig.getFlags();
172             } else if (type == PanelType.DEPRECATED_REMOTE_TABS) {
173                 syncIndex = i;
174                 syncFlags = panelConfig.getFlags();
175             } else if (type == PanelType.COMBINED_HISTORY) {
176                 // Partial landing of bug 1220928 combined the History and Sync panels of users who didn't
177                 // have home panel customizations (including new users), thus they don't this migration.
178                 return jsonPanels;
179             }
180         }
181 
182         if (historyIndex == -1 || syncIndex == -1) {
183             throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
184         }
185 
186         PanelConfig newPanel;
187         int replaceIndex;
188         int removeIndex;
189 
190         if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
191             // Replace the Sync panel if it's visible and the History panel is disabled.
192             replaceIndex = syncIndex;
193             removeIndex = historyIndex;
194             newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, syncFlags);
195         } else {
196             // Otherwise, just replace the History panel.
197             replaceIndex = historyIndex;
198             removeIndex = syncIndex;
199             newPanel = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, historyFlags);
200         }
201 
202         // Copy the array with updated panel and removed panel.
203         final JSONArray newArray = new JSONArray();
204         for (int i = 0; i < jsonPanels.length(); i++) {
205             if (i == replaceIndex) {
206                 newArray.put(newPanel.toJSON());
207             } else if (i == removeIndex) {
208                 continue;
209             } else {
210                 newArray.put(jsonPanels.get(i));
211             }
212         }
213 
214         return newArray;
215     }
216 
217     /**
218      * Iterate over all homepanels to verify that there is at least one default panel. If there is
219      * no default panel, set History as the default panel. (This is only relevant for two botched
220      * migrations where the history panel should have been made the default panel, but wasn't.)
221      */
ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels)222     private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
223         int historyIndex = -1;
224 
225         // If all panels are disabled, there is no default panel - this is the only valid state
226         // that has no default. We can use this flag to track whether any visible panels have been
227         // found.
228         boolean enabledPanelsFound = false;
229 
230         for (int i = 0; i < jsonPanels.length(); i++) {
231             final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
232             if (panelConfig.isDefault()) {
233                 return;
234             }
235 
236             if (!panelConfig.isDisabled()) {
237                 enabledPanelsFound = true;
238             }
239 
240             if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
241                 historyIndex = i;
242             }
243         }
244 
245         if (!enabledPanelsFound) {
246             // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
247             // for more information).
248             return;
249         }
250 
251         // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
252         final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
253         jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
254     }
255 
256     /**
257      * Removes a panel from the home panel config.
258      * If the removed panel was set as the default home panel, we provide a replacement for it.
259      *
260      * @param context Android context
261      * @param jsonPanels array of original JSON panels
262      * @param panelToRemove The home panel to be removed.
263      * @param replacementPanel The panel which will replace it if the removed panel
264      *                         was the default home panel.
265      * @param alwaysUnhide If true, the replacement panel will always be unhidden,
266      *                     otherwise only if we turn it into the new default panel.
267      * @return new array of updated JSON panels
268      * @throws JSONException
269      */
removePanel(Context context, JSONArray jsonPanels, PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide)270     private static JSONArray removePanel(Context context, JSONArray jsonPanels,
271                                          PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
272         boolean wasDefault = false;
273         boolean wasDisabled = false;
274         int replacementPanelIndex = -1;
275         boolean replacementWasDefault = false;
276 
277         // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
278         // the items we don't want deleted into a new array.
279         final JSONArray newJSONPanels = new JSONArray();
280 
281         for (int i = 0; i < jsonPanels.length(); i++) {
282             final JSONObject panelJSON = jsonPanels.getJSONObject(i);
283             final PanelConfig panelConfig = new PanelConfig(panelJSON);
284 
285             if (panelConfig.getType() == panelToRemove) {
286                 // If this panel was the default we'll need to assign a new default:
287                 wasDefault = panelConfig.isDefault();
288                 wasDisabled = panelConfig.isDisabled();
289             } else {
290                 if (panelConfig.getType() == replacementPanel) {
291                     replacementPanelIndex = newJSONPanels.length();
292                     if (panelConfig.isDefault()) {
293                         replacementWasDefault = true;
294                     }
295                 }
296 
297                 newJSONPanels.put(panelJSON);
298             }
299         }
300 
301         // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
302         // to be the new default panel, since a hidden default panel doesn't make sense.
303         // This is to allow preserving the behaviour of the original reading list migration function.
304         if ((wasDefault || alwaysUnhide) && !wasDisabled) {
305             final JSONObject replacementPanelConfig;
306             if (wasDefault) {
307                 // If the removed panel was the default, the replacement has to be made the new default
308                 replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
309             } else {
310                 final EnumSet<HomeConfig.PanelConfig.Flags> flags;
311                 if (replacementWasDefault) {
312                     // However if the replacement panel was already default, we need to preserve it's default status
313                     // (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired
314                     // flags are retained - in this case there's only DEFAULT_PANEL, which is mutually
315                     // exclusive with the DISABLE_PANEL case).
316                     flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL);
317                 } else {
318                     flags = EnumSet.noneOf(PanelConfig.Flags.class);
319                 }
320 
321                 // The panel is visible since we don't set Flags.DISABLED_PANEL.
322                 replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON();
323             }
324 
325             if (replacementPanelIndex != -1) {
326                 newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
327             } else {
328                 newJSONPanels.put(replacementPanelConfig);
329             }
330         }
331 
332         return newJSONPanels;
333     }
334 
335     /**
336      * Checks to see if the reading list panel already exists.
337      *
338      * @param jsonPanels JSONArray array representing the curent set of panel configs.
339      *
340      * @return boolean Whether or not the reading list panel exists.
341      */
readingListPanelExists(JSONArray jsonPanels)342     private static boolean readingListPanelExists(JSONArray jsonPanels) {
343         final int count = jsonPanels.length();
344         for (int i = 0; i < count; i++) {
345             try {
346                 final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
347                 final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
348                 if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
349                     return true;
350                 }
351             } catch (Exception e) {
352                 // It's okay to ignore this exception, since an invalid reading list
353                 // panel config is equivalent to no reading list panel.
354                 Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
355             }
356         }
357         return false;
358     }
359 
360     @CheckResult
migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion, final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor)361     static synchronized JSONArray migratePrefsFromVersionToVersion(final Context context, final int currentVersion, final int newVersion,
362                                                               final JSONArray jsonPanelsIn, final SharedPreferences.Editor prefsEditor) throws JSONException {
363 
364         JSONArray jsonPanels = jsonPanelsIn;
365 
366         for (int v = currentVersion + 1; v <= newVersion; v++) {
367             Log.d(LOGTAG, "Migrating to version = " + v);
368 
369             switch (v) {
370                 case 1:
371                     // Add "Recent Tabs" panel.
372                     addBuiltinPanelConfig(context, jsonPanels,
373                             PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);
374 
375                     // Remove the old pref key.
376                     prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
377                     break;
378 
379                 case 2:
380                     // Add "Remote Tabs"/"Synced Tabs" panel.
381                     addBuiltinPanelConfig(context, jsonPanels,
382                             PanelType.DEPRECATED_REMOTE_TABS, Position.FRONT, Position.BACK);
383                     break;
384 
385                 case 3:
386                     // Add the "Reading List" panel if it does not exist. At one time,
387                     // the Reading List panel was shown only to devices that were not
388                     // considered "low memory". Now, we expose the panel to all devices.
389                     // This migration should only occur for "low memory" devices.
390                     // Note: This will not agree with the default configuration, which
391                     // has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices.
392                     if (!readingListPanelExists(jsonPanels)) {
393                         addBuiltinPanelConfig(context, jsonPanels,
394                                 PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK);
395                     }
396                     break;
397 
398                 case 4:
399                     // Combine the History and Sync panels. In order to minimize an unexpected reordering
400                     // of panels, we try to replace the History panel if it's visible, and fall back to
401                     // the Sync panel if that's visible.
402                     jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
403                     break;
404 
405                 case 5:
406                     // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
407                     ensureDefaultPanelForV5orV8(context, jsonPanels);
408                     break;
409 
410                 case 6:
411                     jsonPanels = removePanel(context, jsonPanels,
412                             PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
413                     break;
414 
415                 case 7:
416                     jsonPanels = removePanel(context, jsonPanels,
417                             PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
418                     break;
419 
420                 case 8:
421                     // Similar to "case 5" above, this time 1304777 - once again we lost track
422                     // of the history panel
423                     ensureDefaultPanelForV5orV8(context, jsonPanels);
424                     break;
425             }
426         }
427 
428         return jsonPanels;
429     }
430 
431     /**
432      * Migrates JSON config data storage.
433      *
434      * @param context Context used to get shared preferences and create built-in panel.
435      * @param jsonString String currently stored in preferences.
436      *
437      * @return JSONArray array representing new set of panel configs.
438      */
maybePerformMigration(Context context, String jsonString)439     private static synchronized JSONArray maybePerformMigration(Context context, String jsonString) throws JSONException {
440         // If the migration is already done, we're at the current version.
441         if (sMigrationDone) {
442             final JSONObject json = new JSONObject(jsonString);
443             return json.getJSONArray(JSON_KEY_PANELS);
444         }
445 
446         // Make sure we only do this version check once.
447         sMigrationDone = true;
448 
449         JSONArray jsonPanels;
450         final int version;
451 
452         final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
453         if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
454             // Our original implementation did not contain versioning, so this is implicitly version 0.
455             jsonPanels = new JSONArray(jsonString);
456             version = 0;
457         } else {
458             final JSONObject json = new JSONObject(jsonString);
459             jsonPanels = json.getJSONArray(JSON_KEY_PANELS);
460             version = json.getInt(JSON_KEY_VERSION);
461         }
462 
463         if (version == VERSION) {
464             return jsonPanels;
465         }
466 
467         Log.d(LOGTAG, "Performing migration");
468 
469         final SharedPreferences.Editor prefsEditor = prefs.edit();
470 
471         jsonPanels = migratePrefsFromVersionToVersion(context, version, VERSION, jsonPanels, prefsEditor);
472 
473         // Save the new panel config and the new version number.
474         final JSONObject newJson = new JSONObject();
475         newJson.put(JSON_KEY_PANELS, jsonPanels);
476         newJson.put(JSON_KEY_VERSION, VERSION);
477 
478         prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
479         prefsEditor.apply();
480 
481         return jsonPanels;
482     }
483 
loadConfigFromString(String jsonString)484     private State loadConfigFromString(String jsonString) {
485         final JSONArray jsonPanelConfigs;
486         try {
487             jsonPanelConfigs = maybePerformMigration(mContext, jsonString);
488             updatePrefsFromConfig(jsonPanelConfigs);
489         } catch (JSONException e) {
490             Log.e(LOGTAG, "Error loading the list of home panels from JSON prefs", e);
491 
492             // Fallback to default config
493             return loadDefaultConfig();
494         }
495 
496         final ArrayList<PanelConfig> panelConfigs = new ArrayList<PanelConfig>();
497 
498         final int count = jsonPanelConfigs.length();
499         for (int i = 0; i < count; i++) {
500             try {
501                 final JSONObject jsonPanelConfig = jsonPanelConfigs.getJSONObject(i);
502                 final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
503                 panelConfigs.add(panelConfig);
504             } catch (Exception e) {
505                 Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
506             }
507         }
508 
509         return new State(panelConfigs, false);
510     }
511 
512     @Override
load()513     public State load() {
514         final SharedPreferences prefs = getSharedPreferences();
515 
516         final String key = (prefs.contains(PREFS_CONFIG_KEY_OLD) ? PREFS_CONFIG_KEY_OLD : PREFS_CONFIG_KEY);
517         final String jsonString = prefs.getString(key, null);
518 
519         final State configState;
520         if (TextUtils.isEmpty(jsonString)) {
521             configState = loadDefaultConfig();
522         } else {
523             configState = loadConfigFromString(jsonString);
524         }
525 
526         return configState;
527     }
528 
529     @Override
save(State configState)530     public void save(State configState) {
531         final SharedPreferences prefs = getSharedPreferences();
532         final SharedPreferences.Editor editor = prefs.edit();
533 
534         // No need to save the state to disk if it represents the default
535         // HomeConfig configuration. Simply force all existing HomeConfigLoader
536         // instances to refresh their contents.
537         if (!configState.isDefault()) {
538             final JSONArray jsonPanelConfigs = new JSONArray();
539 
540             for (PanelConfig panelConfig : configState) {
541                 try {
542                     final JSONObject jsonPanelConfig = panelConfig.toJSON();
543                     jsonPanelConfigs.put(jsonPanelConfig);
544                 } catch (Exception e) {
545                     Log.e(LOGTAG, "Exception converting PanelConfig to JSON", e);
546                 }
547             }
548 
549             try {
550                 final JSONObject json = new JSONObject();
551                 json.put(JSON_KEY_PANELS, jsonPanelConfigs);
552                 json.put(JSON_KEY_VERSION, VERSION);
553 
554                 editor.putString(PREFS_CONFIG_KEY, json.toString());
555             } catch (JSONException e) {
556                 Log.e(LOGTAG, "Exception saving PanelConfig state", e);
557             }
558         }
559 
560         editor.putString(PREFS_LOCALE_KEY, Locale.getDefault().toString());
561         editor.apply();
562 
563         // Trigger reload listeners on all live backend instances
564         sendReloadBroadcast();
565     }
566 
567     @Override
getLocale()568     public String getLocale() {
569         final SharedPreferences prefs = getSharedPreferences();
570 
571         String locale = prefs.getString(PREFS_LOCALE_KEY, null);
572         if (locale == null) {
573             // Initialize config with the current locale
574             final String currentLocale = Locale.getDefault().toString();
575 
576             final SharedPreferences.Editor editor = prefs.edit();
577             editor.putString(PREFS_LOCALE_KEY, currentLocale);
578             editor.apply();
579 
580             // If the user has saved HomeConfig before, return null this
581             // one time to trigger a refresh and ensure we use the
582             // correct locale for the saved state. For more context,
583             // see HomePanelsManager.onLocaleReady().
584             if (!prefs.contains(PREFS_CONFIG_KEY)) {
585                 locale = currentLocale;
586             }
587         }
588 
589         return locale;
590     }
591 
592     @Override
setOnReloadListener(OnReloadListener listener)593     public void setOnReloadListener(OnReloadListener listener) {
594         if (mReloadListener != null) {
595             unregisterReloadReceiver();
596             mReloadBroadcastReceiver = null;
597         }
598 
599         mReloadListener = listener;
600 
601         if (mReloadListener != null) {
602             mReloadBroadcastReceiver = new ReloadBroadcastReceiver();
603             registerReloadReceiver();
604         }
605     }
606 
607     /**
608      * Update prefs that depend on home panels state.
609      *
610      * This includes the prefs that keep track of whether bookmarks or history are enabled, which are
611      * used to control the visibility of the corresponding menu items.
612      */
updatePrefsFromConfig(JSONArray panelsArray)613     private void updatePrefsFromConfig(JSONArray panelsArray) {
614         final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mContext);
615         if (!prefs.contains(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED)
616                 || !prefs.contains(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED)) {
617 
618             final String bookmarkType = PanelType.BOOKMARKS.toString();
619             final String historyType = PanelType.COMBINED_HISTORY.toString();
620             try {
621                 for (int i = 0; i < panelsArray.length(); i++) {
622                     final JSONObject panelObj = panelsArray.getJSONObject(i);
623                     final String panelType = panelObj.optString(PanelConfig.JSON_KEY_TYPE, null);
624                     if (panelType == null) {
625                         break;
626                     }
627                     final boolean isDisabled = panelObj.optBoolean(PanelConfig.JSON_KEY_DISABLED, false);
628                     if (bookmarkType.equals(panelType)) {
629                         prefs.edit().putBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, !isDisabled).apply();
630                     } else if (historyType.equals(panelType)) {
631                         prefs.edit().putBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, !isDisabled).apply();
632                     }
633                 }
634             } catch (JSONException e) {
635                 Log.e(LOGTAG, "Error fetching panel from config to update prefs");
636             }
637         }
638     }
639 
640 
sendReloadBroadcast()641     private void sendReloadBroadcast() {
642         final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
643         final Intent reloadIntent = new Intent(RELOAD_BROADCAST);
644         lbm.sendBroadcast(reloadIntent);
645     }
646 
registerReloadReceiver()647     private void registerReloadReceiver() {
648         final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
649         lbm.registerReceiver(mReloadBroadcastReceiver, new IntentFilter(RELOAD_BROADCAST));
650     }
651 
unregisterReloadReceiver()652     private void unregisterReloadReceiver() {
653         final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);
654         lbm.unregisterReceiver(mReloadBroadcastReceiver);
655     }
656 
657     private class ReloadBroadcastReceiver extends BroadcastReceiver {
658         @Override
onReceive(Context context, Intent intent)659         public void onReceive(Context context, Intent intent) {
660             mReloadListener.onReload();
661         }
662     }
663 }
664